/*!
 * Copyright 2015 Drifty Co.
 * http://drifty.com/
 *
 * Ionic, v1.3.2
 * A powerful HTML5 mobile app framework.
 * http://ionicframework.com/
 *
 * By @maxlynch, @benjsperry, @adamdbradley <3
 *
 * Licensed under the MIT license. Please see LICENSE for more information.
 *
 */

(function () {
    /* eslint no-unused-vars:0 */
    var IonicModule = angular.module('ionic', ['ngAnimate', 'ngSanitize', 'ui.router', 'ngIOS9UIWebViewPatch']),
  extend = angular.extend,
  forEach = angular.forEach,
  isDefined = angular.isDefined,
  isNumber = angular.isNumber,
  isString = angular.isString,
  jqLite = angular.element,
  noop = angular.noop;


    IonicModule.factory('$ionicActionSheet', [
  '$rootScope',
  '$compile',
  '$animate',
  '$timeout',
  '$ionicTemplateLoader',
  '$ionicPlatform',
  '$ionicBody',
  'IONIC_BACK_PRIORITY',
   function ($rootScope, $compile, $animate, $timeout, $ionicTemplateLoader, $ionicPlatform, $ionicBody, IONIC_BACK_PRIORITY) {

       return {
           show: actionSheet
       };

       function actionSheet(opts) {
           var scope = $rootScope.$new(true);

           extend(scope, {
               cancel: noop,
               destructiveButtonClicked: noop,
               buttonClicked: noop,
               $deregisterBackButton: noop,
               buttons: [],
               cancelOnStateChange: true
           }, opts || {});

           function textForIcon(text) {
               if (text && /icon/.test(text)) {
                   scope.$actionSheetHasIcon = true;
               }
           }

           for (var x = 0; x < scope.buttons.length; x++) {
               textForIcon(scope.buttons[x].text);
           }
           textForIcon(scope.cancelText);
           textForIcon(scope.destructiveText);

           // Compile the template
           var element = scope.element = $compile('<ion-action-sheet ng-class="cssClass" buttons="buttons"></ion-action-sheet>')(scope);

           // Grab the sheet element for animation
           var sheetEl = jqLite(element[0].querySelector('.action-sheet-wrapper'));

           var stateChangeListenDone = scope.cancelOnStateChange ?
      $rootScope.$on('$stateChangeSuccess', function () { scope.cancel(); }) :
      noop;

           // removes the actionSheet from the screen
           scope.removeSheet = function (done) {
               if (scope.removed) return;

               scope.removed = true;
               sheetEl.removeClass('action-sheet-up');
               $timeout(function () {
                   // wait to remove this due to a 300ms delay native
                   // click which would trigging whatever was underneath this
                   $ionicBody.removeClass('action-sheet-open');
               }, 400);
               scope.$deregisterBackButton();
               stateChangeListenDone();

               $animate.removeClass(element, 'active').then(function () {
                   scope.$destroy();
                   element.remove();
                   // scope.cancel.$scope is defined near the bottom
                   scope.cancel.$scope = sheetEl = null;
                   (done || noop)(opts.buttons);
               });
           };

           scope.showSheet = function (done) {
               if (scope.removed) return;

               $ionicBody.append(element)
                .addClass('action-sheet-open');

               $animate.addClass(element, 'active').then(function () {
                   if (scope.removed) return;
                   (done || noop)();
               });
               $timeout(function () {
                   if (scope.removed) return;
                   sheetEl.addClass('action-sheet-up');
               }, 20, false);
           };

           // registerBackButtonAction returns a callback to deregister the action
           scope.$deregisterBackButton = $ionicPlatform.registerBackButtonAction(
      function () {
          $timeout(scope.cancel);
      },
      IONIC_BACK_PRIORITY.actionSheet
    );

           // called when the user presses the cancel button
           scope.cancel = function () {
               // after the animation is out, call the cancel callback
               scope.removeSheet(opts.cancel);
           };

           scope.buttonClicked = function (index) {
               // Check if the button click event returned true, which means
               // we can close the action sheet
               if (opts.buttonClicked(index, opts.buttons[index]) === true) {
                   scope.removeSheet();
               }
           };

           scope.destructiveButtonClicked = function () {
               // Check if the destructive button click event returned true, which means
               // we can close the action sheet
               if (opts.destructiveButtonClicked() === true) {
                   scope.removeSheet();
               }
           };

           scope.showSheet();

           // Expose the scope on $ionicActionSheet's return value for the sake
           // of testing it.
           scope.cancel.$scope = scope;

           return scope.cancel;
       }
   } ]);


    jqLite.prototype.addClass = function (cssClasses) {
        var x, y, cssClass, el, splitClasses, existingClasses;
        if (cssClasses && cssClasses != 'ng-scope' && cssClasses != 'ng-isolate-scope') {
            for (x = 0; x < this.length; x++) {
                el = this[x];
                if (el.setAttribute) {

                    if (cssClasses.indexOf(' ') < 0 && el.classList.add) {
                        el.classList.add(cssClasses);
                    } else {
                        existingClasses = (' ' + (el.getAttribute('class') || '') + ' ')
            .replace(/[\n\t]/g, " ");
                        splitClasses = cssClasses.split(' ');

                        for (y = 0; y < splitClasses.length; y++) {
                            cssClass = splitClasses[y].trim();
                            if (existingClasses.indexOf(' ' + cssClass + ' ') === -1) {
                                existingClasses += cssClass + ' ';
                            }
                        }
                        el.setAttribute('class', existingClasses.trim());
                    }
                }
            }
        }
        return this;
    };

    jqLite.prototype.removeClass = function (cssClasses) {
        var x, y, splitClasses, cssClass, el;
        if (cssClasses) {
            for (x = 0; x < this.length; x++) {
                el = this[x];
                if (el.getAttribute) {
                    if (cssClasses.indexOf(' ') < 0 && el.classList.remove) {
                        el.classList.remove(cssClasses);
                    } else {
                        splitClasses = cssClasses.split(' ');

                        for (y = 0; y < splitClasses.length; y++) {
                            cssClass = splitClasses[y];
                            el.setAttribute('class', (
                (" " + (el.getAttribute('class') || '') + " ")
                .replace(/[\n\t]/g, " ")
                .replace(" " + cssClass.trim() + " ", " ")).trim()
            );
                        }
                    }
                }
            }
        }
        return this;
    };


    IonicModule.factory('$ionicBackdrop', [
  '$document', '$timeout', '$$rAF', '$rootScope',
function ($document, $timeout, $$rAF, $rootScope) {

    var el = jqLite('<div class="backdrop">');
    var backdropHolds = 0;

    $document[0].body.appendChild(el[0]);

    return {
        /**
        * @ngdoc method
        * @name $ionicBackdrop#retain
        * @description Retains the backdrop.
        */
        retain: retain,
        /**
        * @ngdoc method
        * @name $ionicBackdrop#release
        * @description
        * Releases the backdrop.
        */
        release: release,

        getElement: getElement,

        // exposed for testing
        _element: el
    };

    function retain() {
        backdropHolds++;
        if (backdropHolds === 1) {
            el.addClass('visible');
            $rootScope.$broadcast('backdrop.shown');
            $$rAF(function () {
                // If we're still at >0 backdropHolds after async...
                if (backdropHolds >= 1) el.addClass('active');
            });
        }
    }
    function release() {
        if (backdropHolds === 1) {
            el.removeClass('active');
            $rootScope.$broadcast('backdrop.hidden');
            $timeout(function () {
                // If we're still at 0 backdropHolds after async...
                if (backdropHolds === 0) el.removeClass('visible');
            }, 400, false);
        }
        backdropHolds = Math.max(0, backdropHolds - 1);
    }

    function getElement() {
        return el;
    }

} ]);

    /**
    * @private
    */
    IonicModule
.factory('$ionicBind', ['$parse', '$interpolate', function ($parse, $interpolate) {
    var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/;
    return function (scope, attrs, bindDefinition) {
        forEach(bindDefinition || {}, function (definition, scopeName) {
            //Adapted from angular.js $compile
            var match = definition.match(LOCAL_REGEXP) || [],
        attrName = match[3] || scopeName,
        mode = match[1], // @, =, or &
        parentGet,
        unwatch;

            switch (mode) {
                case '@':
                    if (!attrs[attrName]) {
                        return;
                    }
                    attrs.$observe(attrName, function (value) {
                        scope[scopeName] = value;
                    });
                    // we trigger an interpolation to ensure
                    // the value is there for use immediately
                    if (attrs[attrName]) {
                        scope[scopeName] = $interpolate(attrs[attrName])(scope);
                    }
                    break;

                case '=':
                    if (!attrs[attrName]) {
                        return;
                    }
                    unwatch = scope.$watch(attrs[attrName], function (value) {
                        scope[scopeName] = value;
                    });
                    //Destroy parent scope watcher when this scope is destroyed
                    scope.$on('$destroy', unwatch);
                    break;

                case '&':
                    /* jshint -W044 */
                    if (attrs[attrName] && attrs[attrName].match(RegExp(scopeName + '\(.*?\)'))) {
                        throw new Error('& expression binding "' + scopeName + '" looks like it will recursively call "' +
                          attrs[attrName] + '" and cause a stack overflow! Please choose a different scopeName.');
                    }
                    parentGet = $parse(attrs[attrName]);
                    scope[scopeName] = function (locals) {
                        return parentGet(scope, locals);
                    };
                    break;
            }
        });
    };
} ]);

    /**
    * @ngdoc service
    * @name $ionicBody
    * @module ionic
    * @description An angular utility service to easily and efficiently
    * add and remove CSS classes from the document's body element.
    */
    IonicModule
.factory('$ionicBody', ['$document', function ($document) {
    return {
        /**
        * @ngdoc method
        * @name $ionicBody#addClass
        * @description Add a class to the document's body element.
        * @param {string} class Each argument will be added to the body element.
        * @returns {$ionicBody} The $ionicBody service so methods can be chained.
        */
        addClass: function () {
            for (var x = 0; x < arguments.length; x++) {
                $document[0].body.classList.add(arguments[x]);
            }
            return this;
        },
        /**
        * @ngdoc method
        * @name $ionicBody#removeClass
        * @description Remove a class from the document's body element.
        * @param {string} class Each argument will be removed from the body element.
        * @returns {$ionicBody} The $ionicBody service so methods can be chained.
        */
        removeClass: function () {
            for (var x = 0; x < arguments.length; x++) {
                $document[0].body.classList.remove(arguments[x]);
            }
            return this;
        },
        /**
        * @ngdoc method
        * @name $ionicBody#enableClass
        * @description Similar to the `add` method, except the first parameter accepts a boolean
        * value determining if the class should be added or removed. Rather than writing user code,
        * such as "if true then add the class, else then remove the class", this method can be
        * given a true or false value which reduces redundant code.
        * @param {boolean} shouldEnableClass A true/false value if the class should be added or removed.
        * @param {string} class Each remaining argument would be added or removed depending on
        * the first argument.
        * @returns {$ionicBody} The $ionicBody service so methods can be chained.
        */
        enableClass: function (shouldEnableClass) {
            var args = Array.prototype.slice.call(arguments).slice(1);
            if (shouldEnableClass) {
                this.addClass.apply(this, args);
            } else {
                this.removeClass.apply(this, args);
            }
            return this;
        },
        /**
        * @ngdoc method
        * @name $ionicBody#append
        * @description Append a child to the document's body.
        * @param {element} element The element to be appended to the body. The passed in element
        * can be either a jqLite element, or a DOM element.
        * @returns {$ionicBody} The $ionicBody service so methods can be chained.
        */
        append: function (ele) {
            $document[0].body.appendChild(ele.length ? ele[0] : ele);
            return this;
        },
        /**
        * @ngdoc method
        * @name $ionicBody#get
        * @description Get the document's body element.
        * @returns {element} Returns the document's body element.
        */
        get: function () {
            return $document[0].body;
        }
    };
} ]);

    IonicModule
.factory('$ionicClickBlock', [
  '$document',
  '$ionicBody',
  '$timeout',
function ($document, $ionicBody, $timeout) {
    var CSS_HIDE = 'click-block-hide';
    var cbEle, fallbackTimer, pendingShow;

    function preventClick(ev) {
        ev.preventDefault();
        ev.stopPropagation();
    }

    function addClickBlock() {
        if (pendingShow) {
            if (cbEle) {
                cbEle.classList.remove(CSS_HIDE);
            } else {
                cbEle = $document[0].createElement('div');
                cbEle.className = 'click-block';
                $ionicBody.append(cbEle);
                cbEle.addEventListener('touchstart', preventClick);
                cbEle.addEventListener('mousedown', preventClick);
            }
            pendingShow = false;
        }
    }

    function removeClickBlock() {
        cbEle && cbEle.classList.add(CSS_HIDE);
    }

    return {
        show: function (autoExpire) {
            pendingShow = true;
            $timeout.cancel(fallbackTimer);
            fallbackTimer = $timeout(this.hide, autoExpire || 310, false);
            addClickBlock();
        },
        hide: function () {
            pendingShow = false;
            $timeout.cancel(fallbackTimer);
            removeClickBlock();
        }
    };
} ]);

    /**
    * @ngdoc service
    * @name $ionicGesture
    * @module ionic
    * @description An angular service exposing ionic
    * {@link ionic.utility:ionic.EventController}'s gestures.
    */
    IonicModule
.factory('$ionicGesture', [function () {
    return {
        /**
        * @ngdoc method
        * @name $ionicGesture#on
        * @description Add an event listener for a gesture on an element. See {@link ionic.utility:ionic.EventController#onGesture}.
        * @param {string} eventType The gesture event to listen for.
        * @param {function(e)} callback The function to call when the gesture
        * happens.
        * @param {element} $element The angular element to listen for the event on.
        * @param {object} options object.
        * @returns {ionic.Gesture} The gesture object (use this to remove the gesture later on).
        */
        on: function (eventType, cb, $element, options) {
            return window.ionic.onGesture(eventType, cb, $element[0], options);
        },
        /**
        * @ngdoc method
        * @name $ionicGesture#off
        * @description Remove an event listener for a gesture on an element. See {@link ionic.utility:ionic.EventController#offGesture}.
        * @param {ionic.Gesture} gesture The gesture that should be removed.
        * @param {string} eventType The gesture event to remove the listener for.
        * @param {function(e)} callback The listener to remove.
        */
        off: function (gesture, eventType, cb) {
            return window.ionic.offGesture(gesture, eventType, cb);
        }
    };
} ]);

    /**
    * @ngdoc service
    * @name $ionicHistory
    * @module ionic
    * @description
    * $ionicHistory keeps track of views as the user navigates through an app. Similar to the way a
    * browser behaves, an Ionic app is able to keep track of the previous view, the current view, and
    * the forward view (if there is one).  However, a typical web browser only keeps track of one
    * history stack in a linear fashion.
    *
    * Unlike a traditional browser environment, apps and webapps have parallel independent histories,
    * such as with tabs. Should a user navigate few pages deep on one tab, and then switch to a new
    * tab and back, the back button relates not to the previous tab, but to the previous pages
    * visited within _that_ tab.
    *
    * `$ionicHistory` facilitates this parallel history architecture.
    */

    IonicModule
.factory('$ionicHistory', [
  '$rootScope',
  '$state',
  '$location',
  '$window',
  '$timeout',
  '$ionicViewSwitcher',
  '$ionicNavViewDelegate',
function ($rootScope, $state, $location, $window, $timeout, $ionicViewSwitcher, $ionicNavViewDelegate) {

    // history actions while navigating views
    var ACTION_INITIAL_VIEW = 'initialView';
    var ACTION_NEW_VIEW = 'newView';
    var ACTION_MOVE_BACK = 'moveBack';
    var ACTION_MOVE_FORWARD = 'moveForward';

    // direction of navigation
    var DIRECTION_BACK = 'back';
    var DIRECTION_FORWARD = 'forward';
    var DIRECTION_ENTER = 'enter';
    var DIRECTION_EXIT = 'exit';
    var DIRECTION_SWAP = 'swap';
    var DIRECTION_NONE = 'none';

    var stateChangeCounter = 0;
    var lastStateId, nextViewOptions, deregisterStateChangeListener, nextViewExpireTimer, forcedNav;

    var viewHistory = {
        histories: { root: { historyId: 'root', parentHistoryId: null, stack: [], cursor: -1} },
        views: {},
        backView: null,
        forwardView: null,
        currentView: null
    };

    var View = function () { };
    View.prototype.initialize = function (data) {
        if (data) {
            for (var name in data) this[name] = data[name];
            return this;
        }
        return null;
    };
    View.prototype.go = function () {

        if (this.stateName) {
            return $state.go(this.stateName, this.stateParams);
        }

        if (this.url && this.url !== $location.url()) {

            if (viewHistory.backView === this) {
                return $window.history.go(-1);
            } else if (viewHistory.forwardView === this) {
                return $window.history.go(1);
            }

            $location.url(this.url);
        }

        return null;
    };
    View.prototype.destroy = function () {
        if (this.scope) {
            this.scope.$destroy && this.scope.$destroy();
            this.scope = null;
        }
    };


    function getViewById(viewId) {
        return (viewId ? viewHistory.views[viewId] : null);
    }

    function getBackView(view) {
        return (view ? getViewById(view.backViewId) : null);
    }

    function getForwardView(view) {
        return (view ? getViewById(view.forwardViewId) : null);
    }

    function getHistoryById(historyId) {
        return (historyId ? viewHistory.histories[historyId] : null);
    }

    function getHistory(scope) {
        var histObj = getParentHistoryObj(scope);

        if (!viewHistory.histories[histObj.historyId]) {
            // this history object exists in parent scope, but doesn't
            // exist in the history data yet
            viewHistory.histories[histObj.historyId] = {
                historyId: histObj.historyId,
                parentHistoryId: getParentHistoryObj(histObj.scope.$parent).historyId,
                stack: [],
                cursor: -1
            };
        }
        return getHistoryById(histObj.historyId);
    }

    function getParentHistoryObj(scope) {
        var parentScope = scope;
        while (parentScope) {
            if (parentScope.hasOwnProperty('$historyId')) {
                // this parent scope has a historyId
                return { historyId: parentScope.$historyId, scope: parentScope };
            }
            // nothing found keep climbing up
            parentScope = parentScope.$parent;
        }
        // no history for the parent, use the root
        return { historyId: 'root', scope: $rootScope };
    }

    function setNavViews(viewId) {
        viewHistory.currentView = getViewById(viewId);
        viewHistory.backView = getBackView(viewHistory.currentView);
        viewHistory.forwardView = getForwardView(viewHistory.currentView);
    }

    function getCurrentStateId() {
        var id;
        if ($state && $state.current && $state.current.name) {
            id = $state.current.name;
            if ($state.params) {
                for (var key in $state.params) {
                    if ($state.params.hasOwnProperty(key) && $state.params[key]) {
                        id += "_" + key + "=" + $state.params[key];
                    }
                }
            }
            return id;
        }
        // if something goes wrong make sure its got a unique stateId
        return ionic.Utils.nextUid();
    }

    function getCurrentStateParams() {
        var rtn;
        if ($state && $state.params) {
            for (var key in $state.params) {
                if ($state.params.hasOwnProperty(key)) {
                    rtn = rtn || {};
                    rtn[key] = $state.params[key];
                }
            }
        }
        return rtn;
    }


    return {

        register: function (parentScope, viewLocals) {

            var currentStateId = getCurrentStateId(),
          hist = getHistory(parentScope),
          currentView = viewHistory.currentView,
          backView = viewHistory.backView,
          forwardView = viewHistory.forwardView,
          viewId = null,
          action = null,
          direction = DIRECTION_NONE,
          historyId = hist.historyId,
          url = $location.url(),
          tmp, x, ele;

            if (lastStateId !== currentStateId) {
                lastStateId = currentStateId;
                stateChangeCounter++;
            }

            if (forcedNav) {
                // we've previously set exactly what to do
                viewId = forcedNav.viewId;
                action = forcedNav.action;
                direction = forcedNav.direction;
                forcedNav = null;

            } else if (backView && backView.stateId === currentStateId) {
                // they went back one, set the old current view as a forward view
                viewId = backView.viewId;
                historyId = backView.historyId;
                action = ACTION_MOVE_BACK;
                if (backView.historyId === currentView.historyId) {
                    // went back in the same history
                    direction = DIRECTION_BACK;

                } else if (currentView) {
                    direction = DIRECTION_EXIT;

                    tmp = getHistoryById(backView.historyId);
                    if (tmp && tmp.parentHistoryId === currentView.historyId) {
                        direction = DIRECTION_ENTER;

                    } else {
                        tmp = getHistoryById(currentView.historyId);
                        if (tmp && tmp.parentHistoryId === hist.parentHistoryId) {
                            direction = DIRECTION_SWAP;
                        }
                    }
                }

            } else if (forwardView && forwardView.stateId === currentStateId) {
                // they went to the forward one, set the forward view to no longer a forward view
                viewId = forwardView.viewId;
                historyId = forwardView.historyId;
                action = ACTION_MOVE_FORWARD;
                if (forwardView.historyId === currentView.historyId) {
                    direction = DIRECTION_FORWARD;

                } else if (currentView) {
                    direction = DIRECTION_EXIT;

                    if (currentView.historyId === hist.parentHistoryId) {
                        direction = DIRECTION_ENTER;

                    } else {
                        tmp = getHistoryById(currentView.historyId);
                        if (tmp && tmp.parentHistoryId === hist.parentHistoryId) {
                            direction = DIRECTION_SWAP;
                        }
                    }
                }

                tmp = getParentHistoryObj(parentScope);
                if (forwardView.historyId && tmp.scope) {
                    // if a history has already been created by the forward view then make sure it stays the same
                    tmp.scope.$historyId = forwardView.historyId;
                    historyId = forwardView.historyId;
                }

            } else if (currentView && currentView.historyId !== historyId &&
                hist.cursor > -1 && hist.stack.length > 0 && hist.cursor < hist.stack.length &&
                hist.stack[hist.cursor].stateId === currentStateId) {
                // they just changed to a different history and the history already has views in it
                var switchToView = hist.stack[hist.cursor];
                viewId = switchToView.viewId;
                historyId = switchToView.historyId;
                action = ACTION_MOVE_BACK;
                direction = DIRECTION_SWAP;

                tmp = getHistoryById(currentView.historyId);
                if (tmp && tmp.parentHistoryId === historyId) {
                    direction = DIRECTION_EXIT;

                } else {
                    tmp = getHistoryById(historyId);
                    if (tmp && tmp.parentHistoryId === currentView.historyId) {
                        direction = DIRECTION_ENTER;
                    }
                }

                // if switching to a different history, and the history of the view we're switching
                // to has an existing back view from a different history than itself, then
                // it's back view would be better represented using the current view as its back view
                tmp = getViewById(switchToView.backViewId);
                if (tmp && switchToView.historyId !== tmp.historyId) {
                    // the new view is being removed from it's old position in the history and being placed at the top,
                    // so we need to update any views that reference it as a backview, otherwise there will be infinitely loops
                    var viewIds = Object.keys(viewHistory.views);
                    viewIds.forEach(function (viewId) {
                        var view = viewHistory.views[viewId];
                        if (view.backViewId === switchToView.viewId) {
                            view.backViewId = null;
                        }
                    });

                    hist.stack[hist.cursor].backViewId = currentView.viewId;
                }

            } else {

                // create an element from the viewLocals template
                ele = $ionicViewSwitcher.createViewEle(viewLocals);
                if (this.isAbstractEle(ele, viewLocals)) {
                    return {
                        action: 'abstractView',
                        direction: DIRECTION_NONE,
                        ele: ele
                    };
                }

                // set a new unique viewId
                viewId = ionic.Utils.nextUid();

                if (currentView) {
                    // set the forward view if there is a current view (ie: if its not the first view)
                    currentView.forwardViewId = viewId;

                    action = ACTION_NEW_VIEW;

                    // check if there is a new forward view within the same history
                    if (forwardView && currentView.stateId !== forwardView.stateId &&
             currentView.historyId === forwardView.historyId) {
                        // they navigated to a new view but the stack already has a forward view
                        // since its a new view remove any forwards that existed
                        tmp = getHistoryById(forwardView.historyId);
                        if (tmp) {
                            // the forward has a history
                            for (x = tmp.stack.length - 1; x >= forwardView.index; x--) {
                                // starting from the end destroy all forwards in this history from this point
                                var stackItem = tmp.stack[x];
                                stackItem && stackItem.destroy && stackItem.destroy();
                                tmp.stack.splice(x);
                            }
                            historyId = forwardView.historyId;
                        }
                    }

                    // its only moving forward if its in the same history
                    if (hist.historyId === currentView.historyId) {
                        direction = DIRECTION_FORWARD;

                    } else if (currentView.historyId !== hist.historyId) {
                        // DB: this is a new view in a different tab
                        direction = DIRECTION_ENTER;

                        tmp = getHistoryById(currentView.historyId);
                        if (tmp && tmp.parentHistoryId === hist.parentHistoryId) {
                            direction = DIRECTION_SWAP;

                        } else {
                            tmp = getHistoryById(tmp.parentHistoryId);
                            if (tmp && tmp.historyId === hist.historyId) {
                                direction = DIRECTION_EXIT;
                            }
                        }
                    }

                } else {
                    // there's no current view, so this must be the initial view
                    action = ACTION_INITIAL_VIEW;
                }

                if (stateChangeCounter < 2) {
                    // views that were spun up on the first load should not animate
                    direction = DIRECTION_NONE;
                }

                // add the new view
                viewHistory.views[viewId] = this.createView({
                    viewId: viewId,
                    index: hist.stack.length,
                    historyId: hist.historyId,
                    backViewId: (currentView && currentView.viewId ? currentView.viewId : null),
                    forwardViewId: null,
                    stateId: currentStateId,
                    stateName: this.currentStateName(),
                    stateParams: getCurrentStateParams(),
                    url: url,
                    canSwipeBack: canSwipeBack(ele, viewLocals)
                });

                // add the new view to this history's stack
                hist.stack.push(viewHistory.views[viewId]);
            }

            deregisterStateChangeListener && deregisterStateChangeListener();
            $timeout.cancel(nextViewExpireTimer);
            if (nextViewOptions) {
                if (nextViewOptions.disableAnimate) direction = DIRECTION_NONE;
                if (nextViewOptions.disableBack) viewHistory.views[viewId].backViewId = null;
                if (nextViewOptions.historyRoot) {
                    for (x = 0; x < hist.stack.length; x++) {
                        if (hist.stack[x].viewId === viewId) {
                            hist.stack[x].index = 0;
                            hist.stack[x].backViewId = hist.stack[x].forwardViewId = null;
                        } else {
                            delete viewHistory.views[hist.stack[x].viewId];
                        }
                    }
                    hist.stack = [viewHistory.views[viewId]];
                }
                nextViewOptions = null;
            }

            setNavViews(viewId);

            if (viewHistory.backView && historyId == viewHistory.backView.historyId && currentStateId == viewHistory.backView.stateId && url == viewHistory.backView.url) {
                for (x = 0; x < hist.stack.length; x++) {
                    if (hist.stack[x].viewId == viewId) {
                        action = 'dupNav';
                        direction = DIRECTION_NONE;
                        if (x > 0) {
                            hist.stack[x - 1].forwardViewId = null;
                        }
                        viewHistory.forwardView = null;
                        viewHistory.currentView.index = viewHistory.backView.index;
                        viewHistory.currentView.backViewId = viewHistory.backView.backViewId;
                        viewHistory.backView = getBackView(viewHistory.backView);
                        hist.stack.splice(x, 1);
                        break;
                    }
                }
            }

            hist.cursor = viewHistory.currentView.index;

            return {
                viewId: viewId,
                action: action,
                direction: direction,
                historyId: historyId,
                enableBack: this.enabledBack(viewHistory.currentView),
                isHistoryRoot: (viewHistory.currentView.index === 0),
                ele: ele
            };
        },

        registerHistory: function (scope) {
            scope.$historyId = ionic.Utils.nextUid();
        },

        createView: function (data) {
            var newView = new View();
            return newView.initialize(data);
        },

        getViewById: getViewById,

        /**
        * @ngdoc method
        * @name $ionicHistory#viewHistory
        * @description The app's view history data, such as all the views and histories, along
        * with how they are ordered and linked together within the navigation stack.
        * @returns {object} Returns an object containing the apps view history data.
        */
        viewHistory: function () {
            return viewHistory;
        },

        /**
        * @ngdoc method
        * @name $ionicHistory#currentView
        * @description The app's current view.
        * @returns {object} Returns the current view.
        */
        currentView: function (view) {
            if (arguments.length) {
                viewHistory.currentView = view;
            }
            return viewHistory.currentView;
        },

        /**
        * @ngdoc method
        * @name $ionicHistory#currentHistoryId
        * @description The ID of the history stack which is the parent container of the current view.
        * @returns {string} Returns the current history ID.
        */
        currentHistoryId: function () {
            return viewHistory.currentView ? viewHistory.currentView.historyId : null;
        },

        /**
        * @ngdoc method
        * @name $ionicHistory#currentTitle
        * @description Gets and sets the current view's title.
        * @param {string=} val The title to update the current view with.
        * @returns {string} Returns the current view's title.
        */
        currentTitle: function (val) {
            if (viewHistory.currentView) {
                if (arguments.length) {
                    viewHistory.currentView.title = val;
                }
                return viewHistory.currentView.title;
            }
        },

        /**
        * @ngdoc method
        * @name $ionicHistory#backView
        * @description Returns the view that was before the current view in the history stack.
        * If the user navigated from View A to View B, then View A would be the back view, and
        * View B would be the current view.
        * @returns {object} Returns the back view.
        */
        backView: function (view) {
            if (arguments.length) {
                viewHistory.backView = view;
            }
            return viewHistory.backView;
        },

        /**
        * @ngdoc method
        * @name $ionicHistory#backTitle
        * @description Gets the back view's title.
        * @returns {string} Returns the back view's title.
        */
        backTitle: function (view) {
            var backView = (view && getViewById(view.backViewId)) || viewHistory.backView;
            return backView && backView.title;
        },

        /**
        * @ngdoc method
        * @name $ionicHistory#forwardView
        * @description Returns the view that was in front of the current view in the history stack.
        * A forward view would exist if the user navigated from View A to View B, then
        * navigated back to View A. At this point then View B would be the forward view, and View
        * A would be the current view.
        * @returns {object} Returns the forward view.
        */
        forwardView: function (view) {
            if (arguments.length) {
                viewHistory.forwardView = view;
            }
            return viewHistory.forwardView;
        },

        /**
        * @ngdoc method
        * @name $ionicHistory#currentStateName
        * @description Returns the current state name.
        * @returns {string}
        */
        currentStateName: function () {
            return ($state && $state.current ? $state.current.name : null);
        },

        isCurrentStateNavView: function (navView) {
            return !!($state && $state.current && $state.current.views && $state.current.views[navView]);
        },

        goToHistoryRoot: function (historyId) {
            if (historyId) {
                var hist = getHistoryById(historyId);
                if (hist && hist.stack.length) {
                    if (viewHistory.currentView && viewHistory.currentView.viewId === hist.stack[0].viewId) {
                        return;
                    }
                    forcedNav = {
                        viewId: hist.stack[0].viewId,
                        action: ACTION_MOVE_BACK,
                        direction: DIRECTION_BACK
                    };
                    hist.stack[0].go();
                }
            }
        },

        /**
        * @ngdoc method
        * @name $ionicHistory#goBack
        * @param {number=} backCount Optional negative integer setting how many views to go
        * back. By default it'll go back one view by using the value `-1`. To go back two
        * views you would use `-2`. If the number goes farther back than the number of views
        * in the current history's stack then it'll go to the first view in the current history's
        * stack. If the number is zero or greater then it'll do nothing. It also does not
        * cross history stacks, meaning it can only go as far back as the current history.
        * @description Navigates the app to the back view, if a back view exists.
        */
        goBack: function (backCount) {
            if (isDefined(backCount) && backCount !== -1) {
                if (backCount > -1) return;

                var currentHistory = viewHistory.histories[this.currentHistoryId()];
                var newCursor = currentHistory.cursor + backCount + 1;
                if (newCursor < 1) {
                    newCursor = 1;
                }

                currentHistory.cursor = newCursor;
                setNavViews(currentHistory.stack[newCursor].viewId);

                var cursor = newCursor - 1;
                var clearStateIds = [];
                var fwdView = getViewById(currentHistory.stack[cursor].forwardViewId);
                while (fwdView) {
                    clearStateIds.push(fwdView.stateId || fwdView.viewId);
                    cursor++;
                    if (cursor >= currentHistory.stack.length) break;
                    fwdView = getViewById(currentHistory.stack[cursor].forwardViewId);
                }

                var self = this;
                if (clearStateIds.length) {
                    $timeout(function () {
                        self.clearCache(clearStateIds);
                    }, 300);
                }
            }

            viewHistory.backView && viewHistory.backView.go();
        },

        /**
        * @ngdoc method
        * @name $ionicHistory#removeBackView
        * @description Remove the previous view from the history completely, including the
        * cached element and scope (if they exist).
        */
        removeBackView: function () {
            var self = this;
            var currentHistory = viewHistory.histories[this.currentHistoryId()];
            var currentCursor = currentHistory.cursor;

            var currentView = currentHistory.stack[currentCursor];
            var backView = currentHistory.stack[currentCursor - 1];
            var replacementView = currentHistory.stack[currentCursor - 2];

            // fail if we dont have enough views in the history
            if (!backView || !replacementView) {
                return;
            }

            // remove the old backView and the cached element/scope
            currentHistory.stack.splice(currentCursor - 1, 1);
            self.clearCache([backView.viewId]);
            // make the replacementView and currentView point to each other (bypass the old backView)
            currentView.backViewId = replacementView.viewId;
            currentView.index = currentView.index - 1;
            replacementView.forwardViewId = currentView.viewId;
            // update the cursor and set new backView
            viewHistory.backView = replacementView;
            currentHistory.currentCursor += -1;
        },

        enabledBack: function (view) {
            var backView = getBackView(view);
            return !!(backView && backView.historyId === view.historyId);
        },

        /**
        * @ngdoc method
        * @name $ionicHistory#clearHistory
        * @description Clears out the app's entire history, except for the current view.
        */
        clearHistory: function () {
            var 
      histories = viewHistory.histories,
      currentView = viewHistory.currentView;

            if (histories) {
                for (var historyId in histories) {

                    if (histories[historyId].stack) {
                        histories[historyId].stack = [];
                        histories[historyId].cursor = -1;
                    }

                    if (currentView && currentView.historyId === historyId) {
                        currentView.backViewId = currentView.forwardViewId = null;
                        histories[historyId].stack.push(currentView);
                    } else if (histories[historyId].destroy) {
                        histories[historyId].destroy();
                    }

                }
            }

            for (var viewId in viewHistory.views) {
                if (viewId !== currentView.viewId) {
                    delete viewHistory.views[viewId];
                }
            }

            if (currentView) {
                setNavViews(currentView.viewId);
            }
        },

        /**
        * @ngdoc method
        * @name $ionicHistory#clearCache
        * @return promise
        * @description Removes all cached views within every {@link ionic.directive:ionNavView}.
        * This both removes the view element from the DOM, and destroy it's scope.
        */
        clearCache: function (stateIds) {
            return $timeout(function () {
                $ionicNavViewDelegate._instances.forEach(function (instance) {
                    instance.clearCache(stateIds);
                });
            });
        },

        /**
        * @ngdoc method
        * @name $ionicHistory#nextViewOptions
        * @description Sets options for the next view. This method can be useful to override
        * certain view/transition defaults right before a view transition happens. For example,
        * the {@link ionic.directive:menuClose} directive uses this method internally to ensure
        * an animated view transition does not happen when a side menu is open, and also sets
        * the next view as the root of its history stack. After the transition these options
        * are set back to null.
        *
        * Available options:
        *
        * * `disableAnimate`: Do not animate the next transition.
        * * `disableBack`: The next view should forget its back view, and set it to null.
        * * `historyRoot`: The next view should become the root view in its history stack.
        *
        * ```js
        * $ionicHistory.nextViewOptions({
        *   disableAnimate: true,
        *   disableBack: true
        * });
        * ```
        */
        nextViewOptions: function (opts) {
            deregisterStateChangeListener && deregisterStateChangeListener();
            if (arguments.length) {
                $timeout.cancel(nextViewExpireTimer);
                if (opts === null) {
                    nextViewOptions = opts;
                } else {
                    nextViewOptions = nextViewOptions || {};
                    extend(nextViewOptions, opts);
                    if (nextViewOptions.expire) {
                        deregisterStateChangeListener = $rootScope.$on('$stateChangeSuccess', function () {
                            nextViewExpireTimer = $timeout(function () {
                                nextViewOptions = null;
                            }, nextViewOptions.expire);
                        });
                    }
                }
            }
            return nextViewOptions;
        },

        isAbstractEle: function (ele, viewLocals) {
            if (viewLocals && viewLocals.$$state && viewLocals.$$state.self['abstract']) {
                return true;
            }
            return !!(ele && (isAbstractTag(ele) || isAbstractTag(ele.children())));
        },

        isActiveScope: function (scope) {
            if (!scope) return false;

            var climbScope = scope;
            var currentHistoryId = this.currentHistoryId();
            var foundHistoryId;

            while (climbScope) {
                if (climbScope.$$disconnected) {
                    return false;
                }

                if (!foundHistoryId && climbScope.hasOwnProperty('$historyId')) {
                    foundHistoryId = true;
                }

                if (currentHistoryId) {
                    if (climbScope.hasOwnProperty('$historyId') && currentHistoryId == climbScope.$historyId) {
                        return true;
                    }
                    if (climbScope.hasOwnProperty('$activeHistoryId')) {
                        if (currentHistoryId == climbScope.$activeHistoryId) {
                            if (climbScope.hasOwnProperty('$historyId')) {
                                return true;
                            }
                            if (!foundHistoryId) {
                                return true;
                            }
                        }
                    }
                }

                if (foundHistoryId && climbScope.hasOwnProperty('$activeHistoryId')) {
                    foundHistoryId = false;
                }

                climbScope = climbScope.$parent;
            }

            return currentHistoryId ? currentHistoryId == 'root' : true;
        }

    };

    function isAbstractTag(ele) {
        return ele && ele.length && /ion-side-menus|ion-tabs/i.test(ele[0].tagName);
    }

    function canSwipeBack(ele, viewLocals) {
        if (viewLocals && viewLocals.$$state && viewLocals.$$state.self.canSwipeBack === false) {
            return false;
        }
        if (ele && ele.attr('can-swipe-back') === 'false') {
            return false;
        }
        var eleChild = ele.find('ion-view');
        if (eleChild && eleChild.attr('can-swipe-back') === 'false') {
            return false;
        }
        return true;
    }

} ])

.run([
  '$rootScope',
  '$state',
  '$location',
  '$document',
  '$ionicPlatform',
  '$ionicHistory',
  'IONIC_BACK_PRIORITY',
function ($rootScope, $state, $location, $document, $ionicPlatform, $ionicHistory, IONIC_BACK_PRIORITY) {

    // always reset the keyboard state when change stage
    $rootScope.$on('$ionicView.beforeEnter', function () {
        ionic.keyboard && ionic.keyboard.hide && ionic.keyboard.hide();
    });

    $rootScope.$on('$ionicHistory.change', function (e, data) {
        if (!data) return null;

        var viewHistory = $ionicHistory.viewHistory();

        var hist = (data.historyId ? viewHistory.histories[data.historyId] : null);
        if (hist && hist.cursor > -1 && hist.cursor < hist.stack.length) {
            // the history they're going to already exists
            // go to it's last view in its stack
            var view = hist.stack[hist.cursor];
            return view.go(data);
        }

        // this history does not have a URL, but it does have a uiSref
        // figure out its URL from the uiSref
        if (!data.url && data.uiSref) {
            data.url = $state.href(data.uiSref);
        }

        if (data.url) {
            // don't let it start with a #, messes with $location.url()
            if (data.url.indexOf('#') === 0) {
                data.url = data.url.replace('#', '');
            }
            if (data.url !== $location.url()) {
                // we've got a good URL, ready GO!
                $location.url(data.url);
            }
        }
    });

    $rootScope.$ionicGoBack = function (backCount) {
        $ionicHistory.goBack(backCount);
    };

    // Set the document title when a new view is shown
    $rootScope.$on('$ionicView.afterEnter', function (ev, data) {
        if (data && data.title) {
            $document[0].title = data.title;
        }
    });

    // Triggered when devices with a hardware back button (Android) is clicked by the user
    // This is a Cordova/Phonegap platform specifc method
    function onHardwareBackButton(e) {
        var backView = $ionicHistory.backView();
        if (backView) {
            // there is a back view, go to it
            backView.go();
        } else {
            // there is no back view, so close the app instead
            ionic.Platform.exitApp();
        }
        e.preventDefault();
        return false;
    }
    $ionicPlatform.registerBackButtonAction(
    onHardwareBackButton,
    IONIC_BACK_PRIORITY.view
  );

} ]);

    /**
    * @ngdoc provider
    * @name $ionicConfigProvider
    * @module ionic
    * @description
    * Ionic automatically takes platform configurations into account to adjust things like what
    * transition style to use and whether tab icons should show on the top or bottom. For example,
    * iOS will move forward by transitioning the entering view from right to center and the leaving
    * view from center to left. However, Android will transition with the entering view going from
    * bottom to center, covering the previous view, which remains stationary. It should be noted
    * that when a platform is not iOS or Android, then it'll default to iOS. So if you are
    * developing on a desktop browser, it's going to take on iOS default configs.
    *
    * These configs can be changed using the `$ionicConfigProvider` during the configuration phase
    * of your app. Additionally, `$ionicConfig` can also set and get config values during the run
    * phase and within the app itself.
    *
    * By default, all base config variables are set to `'platform'`, which means it'll take on the
    * default config of the platform on which it's running. Config variables can be set at this
    * level so all platforms follow the same setting, rather than its platform config.
    * The following code would set the same config variable for all platforms:
    *
    * ```js
    * $ionicConfigProvider.views.maxCache(10);
    * ```
    *
    * Additionally, each platform can have it's own config within the `$ionicConfigProvider.platform`
    * property. The config below would only apply to Android devices.
    *
    * ```js
    * $ionicConfigProvider.platform.android.views.maxCache(5);
    * ```
    *
    * @usage
    * ```js
    * var myApp = angular.module('reallyCoolApp', ['ionic']);
    *
    * myApp.config(function($ionicConfigProvider) {
    *   $ionicConfigProvider.views.maxCache(5);
    *
    *   // note that you can also chain configs
    *   $ionicConfigProvider.backButton.text('Go Back').icon('ion-chevron-left');
    * });
    * ```
    */

    /**
    * @ngdoc method
    * @name $ionicConfigProvider#views.transition
    * @description Animation style when transitioning between views. Default `platform`.
    *
    * @param {string} transition Which style of view transitioning to use.
    *
    * * `platform`: Dynamically choose the correct transition style depending on the platform
    * the app is running from. If the platform is not `ios` or `android` then it will default
    * to `ios`.
    * * `ios`: iOS style transition.
    * * `android`: Android style transition.
    * * `none`: Do not perform animated transitions.
    *
    * @returns {string} value
    */

    /**
    * @ngdoc method
    * @name $ionicConfigProvider#views.maxCache
    * @description  Maximum number of view elements to cache in the DOM. When the max number is
    * exceeded, the view with the longest time period since it was accessed is removed. Views that
    * stay in the DOM cache the view's scope, current state, and scroll position. The scope is
    * disconnected from the `$watch` cycle when it is cached and reconnected when it enters again.
    * When the maximum cache is `0`, the leaving view's element will be removed from the DOM after
    * each view transition, and the next time the same view is shown, it will have to re-compile,
    * attach to the DOM, and link the element again. This disables caching, in effect.
    * @param {number} maxNumber Maximum number of views to retain. Default `10`.
    * @returns {number} How many views Ionic will hold onto until the a view is removed.
    */

    /**
    * @ngdoc method
    * @name $ionicConfigProvider#views.forwardCache
    * @description  By default, when navigating, views that were recently visited are cached, and
    * the same instance data and DOM elements are referenced when navigating back. However, when
    * navigating back in the history, the "forward" views are removed from the cache. If you
    * navigate forward to the same view again, it'll create a new DOM element and controller
    * instance. Basically, any forward views are reset each time. Set this config to `true` to have
    * forward views cached and not reset on each load.
    * @param {boolean} value
    * @returns {boolean}
    */

    /**
    * @ngdoc method
    * @name $ionicConfigProvider#views.swipeBackEnabled
    * @description  By default on iOS devices, swipe to go back functionality is enabled by default.
    * This method can be used to disable it globally, or on a per-view basis.
    * Note: This functionality is only supported on iOS.
    * @param {boolean} value
    * @returns {boolean}
    */

    /**
    * @ngdoc method
    * @name $ionicConfigProvider#scrolling.jsScrolling
    * @description  Whether to use JS or Native scrolling. Defaults to native scrolling. Setting this to
    * `true` has the same effect as setting each `ion-content` to have `overflow-scroll='false'`.
    * @param {boolean} value Defaults to `false` as of Ionic 1.2
    * @returns {boolean}
    */

    /**
    * @ngdoc method
    * @name $ionicConfigProvider#backButton.icon
    * @description Back button icon.
    * @param {string} value
    * @returns {string}
    */

    /**
    * @ngdoc method
    * @name $ionicConfigProvider#backButton.text
    * @description Back button text.
    * @param {string} value Defaults to `Back`.
    * @returns {string}
    */

    /**
    * @ngdoc method
    * @name $ionicConfigProvider#backButton.previousTitleText
    * @description If the previous title text should become the back button text. This
    * is the default for iOS.
    * @param {boolean} value
    * @returns {boolean}
    */

    /**
    * @ngdoc method
    * @name $ionicConfigProvider#form.checkbox
    * @description Checkbox style. Android defaults to `square` and iOS defaults to `circle`.
    * @param {string} value
    * @returns {string}
    */

    /**
    * @ngdoc method
    * @name $ionicConfigProvider#form.toggle
    * @description Toggle item style. Android defaults to `small` and iOS defaults to `large`.
    * @param {string} value
    * @returns {string}
    */

    /**
    * @ngdoc method
    * @name $ionicConfigProvider#spinner.icon
    * @description Default spinner icon to use.
    * @param {string} value Can be: `android`, `ios`, `ios-small`, `bubbles`, `circles`, `crescent`,
    * `dots`, `lines`, `ripple`, or `spiral`.
    * @returns {string}
    */

    /**
    * @ngdoc method
    * @name $ionicConfigProvider#tabs.style
    * @description Tab style. Android defaults to `striped` and iOS defaults to `standard`.
    * @param {string} value Available values include `striped` and `standard`.
    * @returns {string}
    */

    /**
    * @ngdoc method
    * @name $ionicConfigProvider#tabs.position
    * @description Tab position. Android defaults to `top` and iOS defaults to `bottom`.
    * @param {string} value Available values include `top` and `bottom`.
    * @returns {string}
    */

    /**
    * @ngdoc method
    * @name $ionicConfigProvider#templates.maxPrefetch
    * @description Sets the maximum number of templates to prefetch from the templateUrls defined in
    * $stateProvider.state. If set to `0`, the user will have to wait
    * for a template to be fetched the first time when navigating to a new page. Default `30`.
    * @param {integer} value Max number of template to prefetch from the templateUrls defined in
    * `$stateProvider.state()`.
    * @returns {integer}
    */

    /**
    * @ngdoc method
    * @name $ionicConfigProvider#navBar.alignTitle
    * @description Which side of the navBar to align the title. Default `center`.
    *
    * @param {string} value side of the navBar to align the title.
    *
    * * `platform`: Dynamically choose the correct title style depending on the platform
    * the app is running from. If the platform is `ios`, it will default to `center`.
    * If the platform is `android`, it will default to `left`. If the platform is not
    * `ios` or `android`, it will default to `center`.
    *
    * * `left`: Left align the title in the navBar
    * * `center`: Center align the title in the navBar
    * * `right`: Right align the title in the navBar.
    *
    * @returns {string} value
    */

    /**
    * @ngdoc method
    * @name $ionicConfigProvider#navBar.positionPrimaryButtons
    * @description Which side of the navBar to align the primary navBar buttons. Default `left`.
    *
    * @param {string} value side of the navBar to align the primary navBar buttons.
    *
    * * `platform`: Dynamically choose the correct title style depending on the platform
    * the app is running from. If the platform is `ios`, it will default to `left`.
    * If the platform is `android`, it will default to `right`. If the platform is not
    * `ios` or `android`, it will default to `left`.
    *
    * * `left`: Left align the primary navBar buttons in the navBar
    * * `right`: Right align the primary navBar buttons in the navBar.
    *
    * @returns {string} value
    */

    /**
    * @ngdoc method
    * @name $ionicConfigProvider#navBar.positionSecondaryButtons
    * @description Which side of the navBar to align the secondary navBar buttons. Default `right`.
    *
    * @param {string} value side of the navBar to align the secondary navBar buttons.
    *
    * * `platform`: Dynamically choose the correct title style depending on the platform
    * the app is running from. If the platform is `ios`, it will default to `right`.
    * If the platform is `android`, it will default to `right`. If the platform is not
    * `ios` or `android`, it will default to `right`.
    *
    * * `left`: Left align the secondary navBar buttons in the navBar
    * * `right`: Right align the secondary navBar buttons in the navBar.
    *
    * @returns {string} value
    */

    IonicModule
.provider('$ionicConfig', function () {

    var provider = this;
    provider.platform = {};
    var PLATFORM = 'platform';

    var configProperties = {
        views: {
            maxCache: PLATFORM,
            forwardCache: PLATFORM,
            transition: PLATFORM,
            swipeBackEnabled: PLATFORM,
            swipeBackHitWidth: PLATFORM
        },
        navBar: {
            alignTitle: PLATFORM,
            positionPrimaryButtons: PLATFORM,
            positionSecondaryButtons: PLATFORM,
            transition: PLATFORM
        },
        backButton: {
            icon: PLATFORM,
            text: PLATFORM,
            previousTitleText: PLATFORM
        },
        form: {
            checkbox: PLATFORM,
            toggle: PLATFORM
        },
        scrolling: {
            jsScrolling: PLATFORM
        },
        spinner: {
            icon: PLATFORM
        },
        tabs: {
            style: PLATFORM,
            position: PLATFORM
        },
        templates: {
            maxPrefetch: PLATFORM
        },
        platform: {}
    };
    createConfig(configProperties, provider, '');



    // Default
    // -------------------------
    setPlatformConfig('default', {

        views: {
            maxCache: 10,
            forwardCache: false,
            transition: 'ios',
            swipeBackEnabled: true,
            swipeBackHitWidth: 45
        },

        navBar: {
            alignTitle: 'center',
            positionPrimaryButtons: 'left',
            positionSecondaryButtons: 'right',
            transition: 'view'
        },

        backButton: {
            icon: 'ion-ios-arrow-back',
            text: 'Back',
            previousTitleText: true
        },

        form: {
            checkbox: 'circle',
            toggle: 'large'
        },

        scrolling: {
            jsScrolling: true
        },

        spinner: {
            icon: 'ios'
        },

        tabs: {
            style: 'standard',
            position: 'bottom'
        },

        templates: {
            maxPrefetch: 30
        }

    });



    // iOS (it is the default already)
    // -------------------------
    setPlatformConfig('ios', {});



    // Android
    // -------------------------
    setPlatformConfig('android', {

        views: {
            transition: 'android',
            swipeBackEnabled: false
        },

        navBar: {
            alignTitle: 'left',
            positionPrimaryButtons: 'right',
            positionSecondaryButtons: 'right'
        },

        backButton: {
            icon: 'ion-android-arrow-back',
            text: false,
            previousTitleText: false
        },

        form: {
            checkbox: 'square',
            toggle: 'small'
        },

        spinner: {
            icon: 'android'
        },

        tabs: {
            style: 'striped',
            position: 'top'
        },

        scrolling: {
            jsScrolling: false
        }
    });

    // Windows Phone
    // -------------------------
    setPlatformConfig('windowsphone', {
        //scrolling: {
        //  jsScrolling: false
        //}
        spinner: {
            icon: 'android'
        }
    });


    provider.transitions = {
        views: {},
        navBar: {}
    };


    // iOS Transitions
    // -----------------------
    provider.transitions.views.ios = function (enteringEle, leavingEle, direction, shouldAnimate) {

        function setStyles(ele, opacity, x, boxShadowOpacity) {
            var css = {};
            css[ionic.CSS.TRANSITION_DURATION] = d.shouldAnimate ? '' : 0;
            css.opacity = opacity;
            if (boxShadowOpacity > -1) {
                css.boxShadow = '0 0 10px rgba(0,0,0,' + (d.shouldAnimate ? boxShadowOpacity * 0.45 : 0.3) + ')';
            }
            css[ionic.CSS.TRANSFORM] = 'translate3d(' + x + '%,0,0)';
            ionic.DomUtil.cachedStyles(ele, css);
        }

        var d = {
            run: function (step) {
                if (direction == 'forward') {
                    setStyles(enteringEle, 1, (1 - step) * 99, 1 - step); // starting at 98% prevents a flicker
                    setStyles(leavingEle, (1 - 0.1 * step), step * -33, -1);

                } else if (direction == 'back') {
                    setStyles(enteringEle, (1 - 0.1 * (1 - step)), (1 - step) * -33, -1);
                    setStyles(leavingEle, 1, step * 100, 1 - step);

                } else {
                    // swap, enter, exit
                    setStyles(enteringEle, 1, 0, -1);
                    setStyles(leavingEle, 0, 0, -1);
                }
            },
            shouldAnimate: shouldAnimate && (direction == 'forward' || direction == 'back')
        };

        return d;
    };

    provider.transitions.navBar.ios = function (enteringHeaderBar, leavingHeaderBar, direction, shouldAnimate) {

        function setStyles(ctrl, opacity, titleX, backTextX) {
            var css = {};
            css[ionic.CSS.TRANSITION_DURATION] = d.shouldAnimate ? '' : '0ms';
            css.opacity = opacity === 1 ? '' : opacity;

            ctrl.setCss('buttons-left', css);
            ctrl.setCss('buttons-right', css);
            ctrl.setCss('back-button', css);

            css[ionic.CSS.TRANSFORM] = 'translate3d(' + backTextX + 'px,0,0)';
            ctrl.setCss('back-text', css);

            css[ionic.CSS.TRANSFORM] = 'translate3d(' + titleX + 'px,0,0)';
            ctrl.setCss('title', css);
        }

        function enter(ctrlA, ctrlB, step) {
            if (!ctrlA || !ctrlB) return;
            var titleX = (ctrlA.titleTextX() + ctrlA.titleWidth()) * (1 - step);
            var backTextX = (ctrlB && (ctrlB.titleTextX() - ctrlA.backButtonTextLeft()) * (1 - step)) || 0;
            setStyles(ctrlA, step, titleX, backTextX);
        }

        function leave(ctrlA, ctrlB, step) {
            if (!ctrlA || !ctrlB) return;
            var titleX = (-(ctrlA.titleTextX() - ctrlB.backButtonTextLeft()) - (ctrlA.titleLeftRight())) * step;
            setStyles(ctrlA, 1 - step, titleX, 0);
        }

        var d = {
            run: function (step) {
                var enteringHeaderCtrl = enteringHeaderBar.controller();
                var leavingHeaderCtrl = leavingHeaderBar && leavingHeaderBar.controller();
                if (d.direction == 'back') {
                    leave(enteringHeaderCtrl, leavingHeaderCtrl, 1 - step);
                    enter(leavingHeaderCtrl, enteringHeaderCtrl, 1 - step);
                } else {
                    enter(enteringHeaderCtrl, leavingHeaderCtrl, step);
                    leave(leavingHeaderCtrl, enteringHeaderCtrl, step);
                }
            },
            direction: direction,
            shouldAnimate: shouldAnimate && (direction == 'forward' || direction == 'back')
        };

        return d;
    };


    // Android Transitions
    // -----------------------

    provider.transitions.views.android = function (enteringEle, leavingEle, direction, shouldAnimate) {
        shouldAnimate = shouldAnimate && (direction == 'forward' || direction == 'back');

        function setStyles(ele, x, opacity) {
            var css = {};
            css[ionic.CSS.TRANSITION_DURATION] = d.shouldAnimate ? '' : 0;
            css[ionic.CSS.TRANSFORM] = 'translate3d(' + x + '%,0,0)';
            css.opacity = opacity;
            ionic.DomUtil.cachedStyles(ele, css);
        }

        var d = {
            run: function (step) {
                if (direction == 'forward') {
                    setStyles(enteringEle, (1 - step) * 99, 1); // starting at 98% prevents a flicker
                    setStyles(leavingEle, step * -100, 1);

                } else if (direction == 'back') {
                    setStyles(enteringEle, (1 - step) * -100, 1);
                    setStyles(leavingEle, step * 100, 1);

                } else {
                    // swap, enter, exit
                    setStyles(enteringEle, 0, 1);
                    setStyles(leavingEle, 0, 0);
                }
            },
            shouldAnimate: shouldAnimate
        };

        return d;
    };

    provider.transitions.navBar.android = function (enteringHeaderBar, leavingHeaderBar, direction, shouldAnimate) {

        function setStyles(ctrl, opacity) {
            if (!ctrl) return;
            var css = {};
            css.opacity = opacity === 1 ? '' : opacity;

            ctrl.setCss('buttons-left', css);
            ctrl.setCss('buttons-right', css);
            ctrl.setCss('back-button', css);
            ctrl.setCss('back-text', css);
            ctrl.setCss('title', css);
        }

        return {
            run: function (step) {
                setStyles(enteringHeaderBar.controller(), step);
                setStyles(leavingHeaderBar && leavingHeaderBar.controller(), 1 - step);
            },
            shouldAnimate: shouldAnimate && (direction == 'forward' || direction == 'back')
        };
    };


    // No Transition
    // -----------------------

    provider.transitions.views.none = function (enteringEle, leavingEle) {
        return {
            run: function (step) {
                provider.transitions.views.android(enteringEle, leavingEle, false, false).run(step);
            },
            shouldAnimate: false
        };
    };

    provider.transitions.navBar.none = function (enteringHeaderBar, leavingHeaderBar) {
        return {
            run: function (step) {
                provider.transitions.navBar.ios(enteringHeaderBar, leavingHeaderBar, false, false).run(step);
                provider.transitions.navBar.android(enteringHeaderBar, leavingHeaderBar, false, false).run(step);
            },
            shouldAnimate: false
        };
    };


    // private: used to set platform configs
    function setPlatformConfig(platformName, platformConfigs) {
        configProperties.platform[platformName] = platformConfigs;
        provider.platform[platformName] = {};

        addConfig(configProperties, configProperties.platform[platformName]);

        createConfig(configProperties.platform[platformName], provider.platform[platformName], '');
    }


    // private: used to recursively add new platform configs
    function addConfig(configObj, platformObj) {
        for (var n in configObj) {
            if (n != PLATFORM && configObj.hasOwnProperty(n)) {
                if (angular.isObject(configObj[n])) {
                    if (!isDefined(platformObj[n])) {
                        platformObj[n] = {};
                    }
                    addConfig(configObj[n], platformObj[n]);

                } else if (!isDefined(platformObj[n])) {
                    platformObj[n] = null;
                }
            }
        }
    }


    // private: create methods for each config to get/set
    function createConfig(configObj, providerObj, platformPath) {
        forEach(configObj, function (value, namespace) {

            if (angular.isObject(configObj[namespace])) {
                // recursively drill down the config object so we can create a method for each one
                providerObj[namespace] = {};
                createConfig(configObj[namespace], providerObj[namespace], platformPath + '.' + namespace);

            } else {
                // create a method for the provider/config methods that will be exposed
                providerObj[namespace] = function (newValue) {
                    if (arguments.length) {
                        configObj[namespace] = newValue;
                        return providerObj;
                    }
                    if (configObj[namespace] == PLATFORM) {
                        // if the config is set to 'platform', then get this config's platform value
                        var platformConfig = stringObj(configProperties.platform, ionic.Platform.platform() + platformPath + '.' + namespace);
                        if (platformConfig || platformConfig === false) {
                            return platformConfig;
                        }
                        // didnt find a specific platform config, now try the default
                        return stringObj(configProperties.platform, 'default' + platformPath + '.' + namespace);
                    }
                    return configObj[namespace];
                };
            }

        });
    }

    function stringObj(obj, str) {
        str = str.split(".");
        for (var i = 0; i < str.length; i++) {
            if (obj && isDefined(obj[str[i]])) {
                obj = obj[str[i]];
            } else {
                return null;
            }
        }
        return obj;
    }

    provider.setPlatformConfig = setPlatformConfig;


    // private: Service definition for internal Ionic use
    /**
    * @ngdoc service
    * @name $ionicConfig
    * @module ionic
    * @private
    */
    provider.$get = function () {
        return provider;
    };
})
    // Fix for URLs in Cordova apps on Windows Phone
    // http://blogs.msdn.com/b/msdn_answers/archive/2015/02/10/
    // running-cordova-apps-on-windows-and-windows-phone-8-1-using-ionic-angularjs-and-other-frameworks.aspx
.config(['$compileProvider', function ($compileProvider) {
    $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|sms|tel|geo|ftp|mailto|file|ghttps?|ms-appx-web|ms-appx|x-wmapp0):/);
    $compileProvider.imgSrcSanitizationWhitelist(/^\s*(https?|ftp|file|content|blob|ms-appx|ms-appx-web|x-wmapp0):|data:image\//);
} ]);


    var LOADING_TPL =
  '<div class="loading-container">' +
    '<div class="loading">' +
    '</div>' +
  '</div>';

    /**
    * @ngdoc service
    * @name $ionicLoading
    * @module ionic
    * @description
    * An overlay that can be used to indicate activity while blocking user
    * interaction.
    *
    * @usage
    * ```js
    * angular.module('LoadingApp', ['ionic'])
    * .controller('LoadingCtrl', function($scope, $ionicLoading) {
    *   $scope.show = function() {
    *     $ionicLoading.show({
    *       template: 'Loading...',
    *       duration: 3000
    *     }).then(function(){
    *        console.log("The loading indicator is now displayed");
    *     });
    *   };
    *   $scope.hide = function(){
    *     $ionicLoading.hide().then(function(){
    *        console.log("The loading indicator is now hidden");
    *     });
    *   };
    * });
    * ```
    */
    /**
    * @ngdoc object
    * @name $ionicLoadingConfig
    * @module ionic
    * @description
    * Set the default options to be passed to the {@link ionic.service:$ionicLoading} service.
    *
    * @usage
    * ```js
    * var app = angular.module('myApp', ['ionic'])
    * app.constant('$ionicLoadingConfig', {
    *   template: 'Default Loading Template...'
    * });
    * app.controller('AppCtrl', function($scope, $ionicLoading) {
    *   $scope.showLoading = function() {
    *     //options default to values in $ionicLoadingConfig
    *     $ionicLoading.show().then(function(){
    *        console.log("The loading indicator is now displayed");
    *     });
    *   };
    * });
    * ```
    */
    IonicModule
.constant('$ionicLoadingConfig', {
    template: '<ion-spinner></ion-spinner>'
})
.factory('$ionicLoading', [
  '$ionicLoadingConfig',
  '$ionicBody',
  '$ionicTemplateLoader',
  '$ionicBackdrop',
  '$timeout',
  '$q',
  '$log',
  '$compile',
  '$ionicPlatform',
  '$rootScope',
  'IONIC_BACK_PRIORITY',
function ($ionicLoadingConfig, $ionicBody, $ionicTemplateLoader, $ionicBackdrop, $timeout, $q, $log, $compile, $ionicPlatform, $rootScope, IONIC_BACK_PRIORITY) {

    var loaderInstance;
    //default values
    var deregisterBackAction = noop;
    var deregisterStateListener1 = noop;
    var deregisterStateListener2 = noop;
    var loadingShowDelay = $q.when();

    return {
        /**
        * @ngdoc method
        * @name $ionicLoading#show
        * @description Shows a loading indicator. If the indicator is already shown,
        * it will set the options given and keep the indicator shown.
        * @returns {promise} A promise which is resolved when the loading indicator is presented.
        * @param {object} opts The options for the loading indicator. Available properties:
        *  - `{string=}` `template` The html content of the indicator.
        *  - `{string=}` `templateUrl` The url of an html template to load as the content of the indicator.
        *  - `{object=}` `scope` The scope to be a child of. Default: creates a child of $rootScope.
        *  - `{boolean=}` `noBackdrop` Whether to hide the backdrop. By default it will be shown.
        *  - `{boolean=}` `hideOnStateChange` Whether to hide the loading spinner when navigating
        *    to a new state. Default false.
        *  - `{number=}` `delay` How many milliseconds to delay showing the indicator. By default there is no delay.
        *  - `{number=}` `duration` How many milliseconds to wait until automatically
        *  hiding the indicator. By default, the indicator will be shown until `.hide()` is called.
        */
        show: showLoader,
        /**
        * @ngdoc method
        * @name $ionicLoading#hide
        * @description Hides the loading indicator, if shown.
        * @returns {promise} A promise which is resolved when the loading indicator is hidden.
        */
        hide: hideLoader,
        /**
        * @private for testing
        */
        _getLoader: getLoader
    };

    function getLoader() {
        if (!loaderInstance) {
            loaderInstance = $ionicTemplateLoader.compile({
                template: LOADING_TPL,
                appendTo: $ionicBody.get()
            })
      .then(function (self) {
          self.show = function (options) {
              var templatePromise = options.templateUrl ?
            $ionicTemplateLoader.load(options.templateUrl) :
              //options.content: deprecated
            $q.when(options.template || options.content || '');

              self.scope = options.scope || self.scope;

              if (!self.isShown) {
                  //options.showBackdrop: deprecated
                  self.hasBackdrop = !options.noBackdrop && options.showBackdrop !== false;
                  if (self.hasBackdrop) {
                      $ionicBackdrop.retain();
                      $ionicBackdrop.getElement().addClass('backdrop-loading');
                  }
              }

              if (options.duration) {
                  $timeout.cancel(self.durationTimeout);
                  self.durationTimeout = $timeout(
              angular.bind(self, self.hide),
              +options.duration
            );
              }

              deregisterBackAction();
              //Disable hardware back button while loading
              deregisterBackAction = $ionicPlatform.registerBackButtonAction(
            noop,
            IONIC_BACK_PRIORITY.loading
          );

              templatePromise.then(function (html) {
                  if (html) {
                      var loading = self.element.children();
                      loading.html(html);
                      $compile(loading.contents())(self.scope);
                  }

                  //Don't show until template changes
                  if (self.isShown) {
                      self.element.addClass('visible');
                      ionic.requestAnimationFrame(function () {
                          if (self.isShown) {
                              self.element.addClass('active');
                              $ionicBody.addClass('loading-active');
                          }
                      });
                  }
              });

              self.isShown = true;
          };
          self.hide = function () {

              deregisterBackAction();
              if (self.isShown) {
                  if (self.hasBackdrop) {
                      $ionicBackdrop.release();
                      $ionicBackdrop.getElement().removeClass('backdrop-loading');
                  }
                  self.element.removeClass('active');
                  $ionicBody.removeClass('loading-active');
                  self.element.removeClass('visible');
                  ionic.requestAnimationFrame(function () {
                      !self.isShown && self.element.removeClass('visible');
                  });
              }
              $timeout.cancel(self.durationTimeout);
              self.isShown = false;
              var loading = self.element.children();
              loading.html("");
          };

          return self;
      });
        }
        return loaderInstance;
    }

    function showLoader(options) {
        options = extend({}, $ionicLoadingConfig || {}, options || {});
        // use a default delay of 100 to avoid some issues reported on github
        // https://github.com/driftyco/ionic/issues/3717
        var delay = options.delay || options.showDelay || 0;

        deregisterStateListener1();
        deregisterStateListener2();
        if (options.hideOnStateChange) {
            deregisterStateListener1 = $rootScope.$on('$stateChangeSuccess', hideLoader);
            deregisterStateListener2 = $rootScope.$on('$stateChangeError', hideLoader);
        }

        //If loading.show() was called previously, cancel it and show with our new options
        $timeout.cancel(loadingShowDelay);
        loadingShowDelay = $timeout(noop, delay);
        return loadingShowDelay.then(getLoader).then(function (loader) {
            return loader.show(options);
        });
    }

    function hideLoader() {
        deregisterStateListener1();
        deregisterStateListener2();
        $timeout.cancel(loadingShowDelay);
        return getLoader().then(function (loader) {
            return loader.hide();
        });
    }
} ]);

    /**
    * @ngdoc service
    * @name $ionicModal
    * @module ionic
    * @codepen gblny
    * @description
    *
    * Related: {@link ionic.controller:ionicModal ionicModal controller}.
    *
    * The Modal is a content pane that can go over the user's main view
    * temporarily.  Usually used for making a choice or editing an item.
    *
    * Put the content of the modal inside of an `<ion-modal-view>` element.
    *
    * **Notes:**
    * - A modal will broadcast 'modal.shown', 'modal.hidden', and 'modal.removed' events from its originating
    * scope, passing in itself as an event argument. Both the modal.removed and modal.hidden events are
    * called when the modal is removed.
    *
    * - This example assumes your modal is in your main index file or another template file. If it is in its own
    * template file, remove the script tags and call it by file name.
    *
    * @usage
    * ```html
    * <script id="my-modal.html" type="text/ng-template">
    *   <ion-modal-view>
    *     <ion-header-bar>
    *       <h1 class="title">My Modal title</h1>
    *     </ion-header-bar>
    *     <ion-content>
    *       Hello!
    *     </ion-content>
    *   </ion-modal-view>
    * </script>
    * ```
    * ```js
    * angular.module('testApp', ['ionic'])
    * .controller('MyController', function($scope, $ionicModal) {
    *   $ionicModal.fromTemplateUrl('my-modal.html', {
    *     scope: $scope,
    *     animation: 'slide-in-up'
    *   }).then(function(modal) {
    *     $scope.modal = modal;
    *   });
    *   $scope.openModal = function() {
    *     $scope.modal.show();
    *   };
    *   $scope.closeModal = function() {
    *     $scope.modal.hide();
    *   };
    *   // Cleanup the modal when we're done with it!
    *   $scope.$on('$destroy', function() {
    *     $scope.modal.remove();
    *   });
    *   // Execute action on hide modal
    *   $scope.$on('modal.hidden', function() {
    *     // Execute action
    *   });
    *   // Execute action on remove modal
    *   $scope.$on('modal.removed', function() {
    *     // Execute action
    *   });
    * });
    * ```
    */
    IonicModule
.factory('$ionicModal', [
  '$rootScope',
  '$ionicBody',
  '$compile',
  '$timeout',
  '$ionicPlatform',
  '$ionicTemplateLoader',
  '$$q',
  '$log',
  '$ionicClickBlock',
  '$window',
  'IONIC_BACK_PRIORITY',
function ($rootScope, $ionicBody, $compile, $timeout, $ionicPlatform, $ionicTemplateLoader, $$q, $log, $ionicClickBlock, $window, IONIC_BACK_PRIORITY) {

    /**
    * @ngdoc controller
    * @name ionicModal
    * @module ionic
    * @description
    * Instantiated by the {@link ionic.service:$ionicModal} service.
    *
    * Be sure to call [remove()](#remove) when you are done with each modal
    * to clean it up and avoid memory leaks.
    *
    * Note: a modal will broadcast 'modal.shown', 'modal.hidden', and 'modal.removed' events from its originating
    * scope, passing in itself as an event argument. Note: both modal.removed and modal.hidden are
    * called when the modal is removed.
    */
    var ModalView = ionic.views.Modal.inherit({
        /**
        * @ngdoc method
        * @name ionicModal#initialize
        * @description Creates a new modal controller instance.
        * @param {object} options An options object with the following properties:
        *  - `{object=}` `scope` The scope to be a child of.
        *    Default: creates a child of $rootScope.
        *  - `{string=}` `animation` The animation to show & hide with.
        *    Default: 'slide-in-up'
        *  - `{boolean=}` `focusFirstInput` Whether to autofocus the first input of
        *    the modal when shown. Will only show the keyboard on iOS, to force the keyboard to show
        *    on Android, please use the [Ionic keyboard plugin](https://github.com/driftyco/ionic-plugin-keyboard#keyboardshow).
        *    Default: false.
        *  - `{boolean=}` `backdropClickToClose` Whether to close the modal on clicking the backdrop.
        *    Default: true.
        *  - `{boolean=}` `hardwareBackButtonClose` Whether the modal can be closed using the hardware
        *    back button on Android and similar devices.  Default: true.
        */
        initialize: function (opts) {
            ionic.views.Modal.prototype.initialize.call(this, opts);
            this.animation = opts.animation || 'slide-in-up';
        },

        /**
        * @ngdoc method
        * @name ionicModal#show
        * @description Show this modal instance.
        * @returns {promise} A promise which is resolved when the modal is finished animating in.
        */
        show: function (target) {
            var self = this;

            if (self.scope.$$destroyed) {
                $log.error('Cannot call ' + self.viewType + '.show() after remove(). Please create a new ' + self.viewType + ' instance.');
                return $$q.when();
            }

            // on iOS, clicks will sometimes bleed through/ghost click on underlying
            // elements
            $ionicClickBlock.show(600);
            stack.add(self);

            var modalEl = jqLite(self.modalEl);

            self.el.classList.remove('hide');
            $timeout(function () {
                if (!self._isShown) return;
                $ionicBody.addClass(self.viewType + '-open');
            }, 400, false);

            if (!self.el.parentElement) {
                modalEl.addClass(self.animation);
                $ionicBody.append(self.el);
            }

            // if modal was closed while the keyboard was up, reset scroll view on
            // next show since we can only resize it once it's visible
            var scrollCtrl = modalEl.data('$$ionicScrollController');
            scrollCtrl && scrollCtrl.resize();

            if (target && self.positionView) {
                self.positionView(target, modalEl);
                // set up a listener for in case the window size changes

                self._onWindowResize = function () {
                    if (self._isShown) self.positionView(target, modalEl);
                };
                ionic.on('resize', self._onWindowResize, window);
            }

            modalEl.addClass('ng-enter active')
             .removeClass('ng-leave ng-leave-active');

            self._isShown = true;
            self._deregisterBackButton = $ionicPlatform.registerBackButtonAction(
        self.hardwareBackButtonClose ? angular.bind(self, self.hide) : noop,
        IONIC_BACK_PRIORITY.modal
      );

            ionic.views.Modal.prototype.show.call(self);

            $timeout(function () {
                if (!self._isShown) return;
                modalEl.addClass('ng-enter-active');
                ionic.trigger('resize');
                self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.shown', self);
                self.el.classList.add('active');
                self.scope.$broadcast('$ionicHeader.align');
                self.scope.$broadcast('$ionicFooter.align');
                self.scope.$broadcast('$ionic.modalPresented');
            }, 20);

            return $timeout(function () {
                if (!self._isShown) return;
                self.$el.on('touchmove', function (e) {
                    //Don't allow scrolling while open by dragging on backdrop
                    var isInScroll = ionic.DomUtil.getParentOrSelfWithClass(e.target, 'scroll');
                    if (!isInScroll) {
                        e.preventDefault();
                    }
                });
                //After animating in, allow hide on backdrop click
                self.$el.on('click', function (e) {
                    if (self.backdropClickToClose && e.target === self.el && stack.isHighest(self)) {
                        self.hide();
                    }
                });
            }, 400);
        },

        /**
        * @ngdoc method
        * @name ionicModal#hide
        * @description Hide this modal instance.
        * @returns {promise} A promise which is resolved when the modal is finished animating out.
        */
        hide: function () {
            var self = this;
            var modalEl = jqLite(self.modalEl);

            // on iOS, clicks will sometimes bleed through/ghost click on underlying
            // elements
            $ionicClickBlock.show(600);
            stack.remove(self);

            self.el.classList.remove('active');
            modalEl.addClass('ng-leave');

            $timeout(function () {
                if (self._isShown) return;
                modalEl.addClass('ng-leave-active')
               .removeClass('ng-enter ng-enter-active active');

                self.scope.$broadcast('$ionic.modalRemoved');
            }, 20, false);

            self.$el.off('click');
            self._isShown = false;
            self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.hidden', self);
            self._deregisterBackButton && self._deregisterBackButton();

            ionic.views.Modal.prototype.hide.call(self);

            // clean up event listeners
            if (self.positionView) {
                ionic.off('resize', self._onWindowResize, window);
            }

            return $timeout(function () {
                if (!modalStack.length) {
                    $ionicBody.removeClass(self.viewType + '-open');
                }
                self.el.classList.add('hide');
            }, self.hideDelay || 320);
        },

        /**
        * @ngdoc method
        * @name ionicModal#remove
        * @description Remove this modal instance from the DOM and clean up.
        * @returns {promise} A promise which is resolved when the modal is finished animating out.
        */
        remove: function () {
            var self = this,
          deferred, promise;
            self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.removed', self);

            // Only hide modal, when it is actually shown!
            // The hide function shows a click-block-div for a split second, because on iOS,
            // clicks will sometimes bleed through/ghost click on underlying elements.
            // However, this will make the app unresponsive for short amount of time.
            // We don't want that, if the modal window is already hidden.
            if (self._isShown) {
                promise = self.hide();
            } else {
                deferred = $$q.defer();
                deferred.resolve();
                promise = deferred.promise;
            }

            return promise.then(function () {
                self.scope.$destroy();
                self.$el.remove();
            });
        },

        /**
        * @ngdoc method
        * @name ionicModal#isShown
        * @returns boolean Whether this modal is currently shown.
        */
        isShown: function () {
            return !!this._isShown;
        }
    });

    var createModal = function (templateString, options) {
        // Create a new scope for the modal
        var scope = options.scope && options.scope.$new() || $rootScope.$new(true);

        options.viewType = options.viewType || 'modal';

        extend(scope, {
            $hasHeader: false,
            $hasSubheader: false,
            $hasFooter: false,
            $hasSubfooter: false,
            $hasTabs: false,
            $hasTabsTop: false
        });

        // Compile the template
        var element = $compile('<ion-' + options.viewType + '>' + templateString + '</ion-' + options.viewType + '>')(scope);

        options.$el = element;
        options.el = element[0];
        options.modalEl = options.el.querySelector('.' + options.viewType);
        var modal = new ModalView(options);

        modal.scope = scope;

        // If this wasn't a defined scope, we can assign the viewType to the isolated scope
        // we created
        if (!options.scope) {
            scope[options.viewType] = modal;
        }

        return modal;
    };

    var modalStack = [];
    var stack = {
        add: function (modal) {
            modalStack.push(modal);
        },
        remove: function (modal) {
            var index = modalStack.indexOf(modal);
            if (index > -1 && index < modalStack.length) {
                modalStack.splice(index, 1);
            }
        },
        isHighest: function (modal) {
            var index = modalStack.indexOf(modal);
            return (index > -1 && index === modalStack.length - 1);
        }
    };

    return {
        /**
        * @ngdoc method
        * @name $ionicModal#fromTemplate
        * @param {string} templateString The template string to use as the modal's
        * content.
        * @param {object} options Options to be passed {@link ionic.controller:ionicModal#initialize ionicModal#initialize} method.
        * @returns {object} An instance of an {@link ionic.controller:ionicModal}
        * controller.
        */
        fromTemplate: function (templateString, options) {
            var modal = createModal(templateString, options || {});
            return modal;
        },
        /**
        * @ngdoc method
        * @name $ionicModal#fromTemplateUrl
        * @param {string} templateUrl The url to load the template from.
        * @param {object} options Options to be passed {@link ionic.controller:ionicModal#initialize ionicModal#initialize} method.
        * options object.
        * @returns {promise} A promise that will be resolved with an instance of
        * an {@link ionic.controller:ionicModal} controller.
        */
        fromTemplateUrl: function (url, options, _) {
            var cb;
            //Deprecated: allow a callback as second parameter. Now we return a promise.
            if (angular.isFunction(options)) {
                cb = options;
                options = _;
            }
            return $ionicTemplateLoader.load(url).then(function (templateString) {
                var modal = createModal(templateString, options || {});
                cb && cb(modal);
                return modal;
            });
        },

        stack: stack
    };
} ]);


    /**
    * @ngdoc service
    * @name $ionicNavBarDelegate
    * @module ionic
    * @description
    * Delegate for controlling the {@link ionic.directive:ionNavBar} directive.
    *
    * @usage
    *
    * ```html
    * <body ng-controller="MyCtrl">
    *   <ion-nav-bar>
    *     <button ng-click="setNavTitle('banana')">
    *       Set title to banana!
    *     </button>
    *   </ion-nav-bar>
    * </body>
    * ```
    * ```js
    * function MyCtrl($scope, $ionicNavBarDelegate) {
    *   $scope.setNavTitle = function(title) {
    *     $ionicNavBarDelegate.title(title);
    *   }
    * }
    * ```
    */
    IonicModule
.service('$ionicNavBarDelegate', ionic.DelegateService([
    /**
    * @ngdoc method
    * @name $ionicNavBarDelegate#align
    * @description Aligns the title with the buttons in a given direction.
    * @param {string=} direction The direction to the align the title text towards.
    * Available: 'left', 'right', 'center'. Default: 'center'.
    */
  'align',
    /**
    * @ngdoc method
    * @name $ionicNavBarDelegate#showBackButton
    * @description
    * Set/get whether the {@link ionic.directive:ionNavBackButton} is shown
    * (if it exists and there is a previous view that can be navigated to).
    * @param {boolean=} show Whether to show the back button.
    * @returns {boolean} Whether the back button is shown.
    */
  'showBackButton',
    /**
    * @ngdoc method
    * @name $ionicNavBarDelegate#showBar
    * @description
    * Set/get whether the {@link ionic.directive:ionNavBar} is shown.
    * @param {boolean} show Whether to show the bar.
    * @returns {boolean} Whether the bar is shown.
    */
  'showBar',
    /**
    * @ngdoc method
    * @name $ionicNavBarDelegate#title
    * @description
    * Set the title for the {@link ionic.directive:ionNavBar}.
    * @param {string} title The new title to show.
    */
  'title',

    // DEPRECATED, as of v1.0.0-beta14 -------
  'changeTitle',
  'setTitle',
  'getTitle',
  'back',
  'getPreviousTitle'
    // END DEPRECATED -------
]));


    IonicModule
.service('$ionicNavViewDelegate', ionic.DelegateService([
  'clearCache'
]));



    /**
    * @ngdoc service
    * @name $ionicPlatform
    * @module ionic
    * @description
    * An angular abstraction of {@link ionic.utility:ionic.Platform}.
    *
    * Used to detect the current platform, as well as do things like override the
    * Android back button in PhoneGap/Cordova.
    */
    IonicModule
.constant('IONIC_BACK_PRIORITY', {
    view: 100,
    sideMenu: 150,
    modal: 200,
    actionSheet: 300,
    popup: 400,
    loading: 500
})
.provider('$ionicPlatform', function () {
    return {
        $get: ['$q', '$ionicScrollDelegate', function ($q, $ionicScrollDelegate) {
            var self = {

                /**
                * @ngdoc method
                * @name $ionicPlatform#onHardwareBackButton
                * @description
                * Some platforms have a hardware back button, so this is one way to
                * bind to it.
                * @param {function} callback the callback to trigger when this event occurs
                */
                onHardwareBackButton: function (cb) {
                    ionic.Platform.ready(function () {
                        document.addEventListener('backbutton', cb, false);
                    });
                },

                /**
                * @ngdoc method
                * @name $ionicPlatform#offHardwareBackButton
                * @description
                * Remove an event listener for the backbutton.
                * @param {function} callback The listener function that was
                * originally bound.
                */
                offHardwareBackButton: function (fn) {
                    ionic.Platform.ready(function () {
                        document.removeEventListener('backbutton', fn);
                    });
                },

                /**
                * @ngdoc method
                * @name $ionicPlatform#registerBackButtonAction
                * @description
                * Register a hardware back button action. Only one action will execute
                * when the back button is clicked, so this method decides which of
                * the registered back button actions has the highest priority.
                *
                * For example, if an actionsheet is showing, the back button should
                * close the actionsheet, but it should not also go back a page view
                * or close a modal which may be open.
                *
                * The priorities for the existing back button hooks are as follows:
                *   Return to previous view = 100
                *   Close side menu = 150
                *   Dismiss modal = 200
                *   Close action sheet = 300
                *   Dismiss popup = 400
                *   Dismiss loading overlay = 500
                *
                * Your back button action will override each of the above actions
                * whose priority is less than the priority you provide. For example,
                * an action assigned a priority of 101 will override the 'return to
                * previous view' action, but not any of the other actions.
                *
                * @param {function} callback Called when the back button is pressed,
                * if this listener is the highest priority.
                * @param {number} priority Only the highest priority will execute.
                * @param {*=} actionId The id to assign this action. Default: a
                * random unique id.
                * @returns {function} A function that, when called, will deregister
                * this backButtonAction.
                */
                $backButtonActions: {},
                registerBackButtonAction: function (fn, priority, actionId) {

                    if (!self._hasBackButtonHandler) {
                        // add a back button listener if one hasn't been setup yet
                        self.$backButtonActions = {};
                        self.onHardwareBackButton(self.hardwareBackButtonClick);
                        self._hasBackButtonHandler = true;
                    }

                    var action = {
                        id: (actionId ? actionId : ionic.Utils.nextUid()),
                        priority: (priority ? priority : 0),
                        fn: fn
                    };
                    self.$backButtonActions[action.id] = action;

                    // return a function to de-register this back button action
                    return function () {
                        delete self.$backButtonActions[action.id];
                    };
                },

                /**
                * @private
                */
                hardwareBackButtonClick: function (e) {
                    // loop through all the registered back button actions
                    // and only run the last one of the highest priority
                    var priorityAction, actionId;
                    for (actionId in self.$backButtonActions) {
                        if (!priorityAction || self.$backButtonActions[actionId].priority >= priorityAction.priority) {
                            priorityAction = self.$backButtonActions[actionId];
                        }
                    }
                    if (priorityAction) {
                        priorityAction.fn(e);
                        return priorityAction;
                    }
                },

                is: function (type) {
                    return ionic.Platform.is(type);
                },

                /**
                * @ngdoc method
                * @name $ionicPlatform#on
                * @description
                * Add Cordova event listeners, such as `pause`, `resume`, `volumedownbutton`, `batterylow`,
                * `offline`, etc. More information about available event types can be found in
                * [Cordova's event documentation](https://cordova.apache.org/docs/en/latest/cordova/events/events.html).
                * @param {string} type Cordova [event type](https://cordova.apache.org/docs/en/latest/cordova/events/events.html).
                * @param {function} callback Called when the Cordova event is fired.
                * @returns {function} Returns a deregistration function to remove the event listener.
                */
                on: function (type, cb) {
                    ionic.Platform.ready(function () {
                        document.addEventListener(type, cb, false);
                    });
                    return function () {
                        ionic.Platform.ready(function () {
                            document.removeEventListener(type, cb);
                        });
                    };
                },

                /**
                * @ngdoc method
                * @name $ionicPlatform#ready
                * @description
                * Trigger a callback once the device is ready,
                * or immediately if the device is already ready.
                * @param {function=} callback The function to call.
                * @returns {promise} A promise which is resolved when the device is ready.
                */
                ready: function (cb) {
                    var q = $q.defer();

                    ionic.Platform.ready(function () {

                        window.addEventListener('statusTap', function () {
                            $ionicScrollDelegate.scrollTop(true);
                        });

                        q.resolve();
                        cb && cb();
                    });

                    return q.promise;
                }
            };

            return self;
        } ]
    };

});

    /**
    * @ngdoc service
    * @name $ionicPopover
    * @module ionic
    * @description
    *
    * Related: {@link ionic.controller:ionicPopover ionicPopover controller}.
    *
    * The Popover is a view that floats above an app’s content. Popovers provide an
    * easy way to present or gather information from the user and are
    * commonly used in the following situations:
    *
    * - Show more info about the current view
    * - Select a commonly used tool or configuration
    * - Present a list of actions to perform inside one of your views
    *
    * Put the content of the popover inside of an `<ion-popover-view>` element.
    *
    * @usage
    * ```html
    * <p>
    *   <button ng-click="openPopover($event)">Open Popover</button>
    * </p>
    *
    * <script id="my-popover.html" type="text/ng-template">
    *   <ion-popover-view>
    *     <ion-header-bar>
    *       <h1 class="title">My Popover Title</h1>
    *     </ion-header-bar>
    *     <ion-content>
    *       Hello!
    *     </ion-content>
    *   </ion-popover-view>
    * </script>
    * ```
    * ```js
    * angular.module('testApp', ['ionic'])
    * .controller('MyController', function($scope, $ionicPopover) {
    *
    *   // .fromTemplate() method
    *   var template = '<ion-popover-view><ion-header-bar> <h1 class="title">My Popover Title</h1> </ion-header-bar> <ion-content> Hello! </ion-content></ion-popover-view>';
    *
    *   $scope.popover = $ionicPopover.fromTemplate(template, {
    *     scope: $scope
    *   });
    *
    *   // .fromTemplateUrl() method
    *   $ionicPopover.fromTemplateUrl('my-popover.html', {
    *     scope: $scope
    *   }).then(function(popover) {
    *     $scope.popover = popover;
    *   });
    *
    *
    *   $scope.openPopover = function($event) {
    *     $scope.popover.show($event);
    *   };
    *   $scope.closePopover = function() {
    *     $scope.popover.hide();
    *   };
    *   //Cleanup the popover when we're done with it!
    *   $scope.$on('$destroy', function() {
    *     $scope.popover.remove();
    *   });
    *   // Execute action on hidden popover
    *   $scope.$on('popover.hidden', function() {
    *     // Execute action
    *   });
    *   // Execute action on remove popover
    *   $scope.$on('popover.removed', function() {
    *     // Execute action
    *   });
    * });
    * ```
    */


    IonicModule
.factory('$ionicPopover', ['$ionicModal', '$ionicPosition', '$document', '$window',
function ($ionicModal, $ionicPosition, $document, $window) {

    var POPOVER_BODY_PADDING = 6;

    var POPOVER_OPTIONS = {
        viewType: 'popover',
        hideDelay: 1,
        animation: 'none',
        positionView: positionView
    };

    function positionView(target, popoverEle) {
        var targetEle = jqLite(target.target || target);
        var buttonOffset = $ionicPosition.offset(targetEle);
        var popoverWidth = popoverEle.prop('offsetWidth');
        var popoverHeight = popoverEle.prop('offsetHeight');
        // Use innerWidth and innerHeight, because clientWidth and clientHeight
        // doesn't work consistently for body on all platforms
        var bodyWidth = $window.innerWidth;
        var bodyHeight = $window.innerHeight;

        var popoverCSS = {
            left: buttonOffset.left + buttonOffset.width / 2 - popoverWidth / 2
        };
        var arrowEle = jqLite(popoverEle[0].querySelector('.popover-arrow'));

        if (popoverCSS.left < POPOVER_BODY_PADDING) {
            popoverCSS.left = POPOVER_BODY_PADDING;
        } else if (popoverCSS.left + popoverWidth + POPOVER_BODY_PADDING > bodyWidth) {
            popoverCSS.left = bodyWidth - popoverWidth - POPOVER_BODY_PADDING;
        }

        // If the popover when popped down stretches past bottom of screen,
        // make it pop up if there's room above
        if (buttonOffset.top + buttonOffset.height + popoverHeight > bodyHeight &&
        buttonOffset.top - popoverHeight > 0) {
            popoverCSS.top = buttonOffset.top - popoverHeight;
            popoverEle.addClass('popover-bottom');
        } else {
            popoverCSS.top = buttonOffset.top + buttonOffset.height;
            popoverEle.removeClass('popover-bottom');
        }

        arrowEle.css({
            left: buttonOffset.left + buttonOffset.width / 2 -
        arrowEle.prop('offsetWidth') / 2 - popoverCSS.left + 'px'
        });

        popoverEle.css({
            top: popoverCSS.top + 'px',
            left: popoverCSS.left + 'px',
            marginLeft: '0',
            opacity: '1'
        });

    }

    /**
    * @ngdoc controller
    * @name ionicPopover
    * @module ionic
    * @description
    * Instantiated by the {@link ionic.service:$ionicPopover} service.
    *
    * Be sure to call [remove()](#remove) when you are done with each popover
    * to clean it up and avoid memory leaks.
    *
    * Note: a popover will broadcast 'popover.shown', 'popover.hidden', and 'popover.removed' events from its originating
    * scope, passing in itself as an event argument. Both the popover.removed and popover.hidden events are
    * called when the popover is removed.
    */

    /**
    * @ngdoc method
    * @name ionicPopover#initialize
    * @description Creates a new popover controller instance.
    * @param {object} options An options object with the following properties:
    *  - `{object=}` `scope` The scope to be a child of.
    *    Default: creates a child of $rootScope.
    *  - `{boolean=}` `focusFirstInput` Whether to autofocus the first input of
    *    the popover when shown.  Default: false.
    *  - `{boolean=}` `backdropClickToClose` Whether to close the popover on clicking the backdrop.
    *    Default: true.
    *  - `{boolean=}` `hardwareBackButtonClose` Whether the popover can be closed using the hardware
    *    back button on Android and similar devices.  Default: true.
    */

    /**
    * @ngdoc method
    * @name ionicPopover#show
    * @description Show this popover instance.
    * @param {$event} $event The $event or target element which the popover should align
    * itself next to.
    * @returns {promise} A promise which is resolved when the popover is finished animating in.
    */

    /**
    * @ngdoc method
    * @name ionicPopover#hide
    * @description Hide this popover instance.
    * @returns {promise} A promise which is resolved when the popover is finished animating out.
    */

    /**
    * @ngdoc method
    * @name ionicPopover#remove
    * @description Remove this popover instance from the DOM and clean up.
    * @returns {promise} A promise which is resolved when the popover is finished animating out.
    */

    /**
    * @ngdoc method
    * @name ionicPopover#isShown
    * @returns boolean Whether this popover is currently shown.
    */

    return {
        /**
        * @ngdoc method
        * @name $ionicPopover#fromTemplate
        * @param {string} templateString The template string to use as the popovers's
        * content.
        * @param {object} options Options to be passed to the initialize method.
        * @returns {object} An instance of an {@link ionic.controller:ionicPopover}
        * controller (ionicPopover is built on top of $ionicPopover).
        */
        fromTemplate: function (templateString, options) {
            return $ionicModal.fromTemplate(templateString, ionic.Utils.extend({}, POPOVER_OPTIONS, options));
        },
        /**
        * @ngdoc method
        * @name $ionicPopover#fromTemplateUrl
        * @param {string} templateUrl The url to load the template from.
        * @param {object} options Options to be passed to the initialize method.
        * @returns {promise} A promise that will be resolved with an instance of
        * an {@link ionic.controller:ionicPopover} controller (ionicPopover is built on top of $ionicPopover).
        */
        fromTemplateUrl: function (url, options) {
            return $ionicModal.fromTemplateUrl(url, ionic.Utils.extend({}, POPOVER_OPTIONS, options));
        }
    };

} ]);


    var POPUP_TPL =
  '<div class="popup-container" ng-class="cssClass">' +
    '<div class="popup">' +
      '<div class="popup-head">' +
        '<h3 class="popup-title" ng-bind-html="title"></h3>' +
        '<h5 class="popup-sub-title" ng-bind-html="subTitle" ng-if="subTitle"></h5>' +
      '</div>' +
      '<div class="popup-body">' +
      '</div>' +
      '<div class="popup-buttons" ng-show="buttons.length">' +
        '<button ng-repeat="button in buttons" ng-click="$buttonTapped(button, $event)" class="button" ng-class="button.type || \'button-default\'" ng-bind-html="button.text"></button>' +
      '</div>' +
    '</div>' +
  '</div>';

    /**
    * @ngdoc service
    * @name $ionicPopup
    * @module ionic
    * @restrict E
    * @codepen zkmhJ
    * @description
    *
    * The Ionic Popup service allows programmatically creating and showing popup
    * windows that require the user to respond in order to continue.
    *
    * The popup system has support for more flexible versions of the built in `alert()`, `prompt()`,
    * and `confirm()` functions that users are used to, in addition to allowing popups with completely
    * custom content and look.
    *
    * An input can be given an `autofocus` attribute so it automatically receives focus when
    * the popup first shows. However, depending on certain use-cases this can cause issues with
    * the tap/click system, which is why Ionic prefers using the `autofocus` attribute as
    * an opt-in feature and not the default.
    *
    * @usage
    * A few basic examples, see below for details about all of the options available.
    *
    * ```js
    *angular.module('mySuperApp', ['ionic'])
    *.controller('PopupCtrl',function($scope, $ionicPopup, $timeout) {
    *
    * // Triggered on a button click, or some other target
    * $scope.showPopup = function() {
    *   $scope.data = {};
    *
    *   // An elaborate, custom popup
    *   var myPopup = $ionicPopup.show({
    *     template: '<input type="password" ng-model="data.wifi">',
    *     title: 'Enter Wi-Fi Password',
    *     subTitle: 'Please use normal things',
    *     scope: $scope,
    *     buttons: [
    *       { text: 'Cancel' },
    *       {
    *         text: '<b>Save</b>',
    *         type: 'button-positive',
    *         onTap: function(e) {
    *           if (!$scope.data.wifi) {
    *             //don't allow the user to close unless he enters wifi password
    *             e.preventDefault();
    *           } else {
    *             return $scope.data.wifi;
    *           }
    *         }
    *       }
    *     ]
    *   });
    *
    *   myPopup.then(function(res) {
    *     console.log('Tapped!', res);
    *   });
    *
    *   $timeout(function() {
    *      myPopup.close(); //close the popup after 3 seconds for some reason
    *   }, 3000);
    *  };
    *
    *  // A confirm dialog
    *  $scope.showConfirm = function() {
    *    var confirmPopup = $ionicPopup.confirm({
    *      title: 'Consume Ice Cream',
    *      template: 'Are you sure you want to eat this ice cream?'
    *    });
    *
    *    confirmPopup.then(function(res) {
    *      if(res) {
    *        console.log('You are sure');
    *      } else {
    *        console.log('You are not sure');
    *      }
    *    });
    *  };
    *
    *  // An alert dialog
    *  $scope.showAlert = function() {
    *    var alertPopup = $ionicPopup.alert({
    *      title: 'Don\'t eat that!',
    *      template: 'It might taste good'
    *    });
    *
    *    alertPopup.then(function(res) {
    *      console.log('Thank you for not eating my delicious ice cream cone');
    *    });
    *  };
    *});
    *```
    */

    IonicModule
.factory('$ionicPopup', [
  '$ionicTemplateLoader',
  '$ionicBackdrop',
  '$q',
  '$timeout',
  '$rootScope',
  '$ionicBody',
  '$compile',
  '$ionicPlatform',
  '$ionicModal',
  'IONIC_BACK_PRIORITY',
function ($ionicTemplateLoader, $ionicBackdrop, $q, $timeout, $rootScope, $ionicBody, $compile, $ionicPlatform, $ionicModal, IONIC_BACK_PRIORITY) {
    //TODO allow this to be configured
    var config = {
        stackPushDelay: 75
    };
    var popupStack = [];

    var $ionicPopup = {
        /**
        * @ngdoc method
        * @description
        * Show a complex popup. This is the master show function for all popups.
        *
        * A complex popup has a `buttons` array, with each button having a `text` and `type`
        * field, in addition to an `onTap` function.  The `onTap` function, called when
        * the corresponding button on the popup is tapped, will by default close the popup
        * and resolve the popup promise with its return value.  If you wish to prevent the
        * default and keep the popup open on button tap, call `event.preventDefault()` on the
        * passed in tap event.  Details below.
        *
        * @name $ionicPopup#show
        * @param {object} options The options for the new popup, of the form:
        *
        * ```
        * {
        *   title: '', // String. The title of the popup.
        *   cssClass: '', // String, The custom CSS class name
        *   subTitle: '', // String (optional). The sub-title of the popup.
        *   template: '', // String (optional). The html template to place in the popup body.
        *   templateUrl: '', // String (optional). The URL of an html template to place in the popup   body.
        *   scope: null, // Scope (optional). A scope to link to the popup content.
        *   buttons: [{ // Array[Object] (optional). Buttons to place in the popup footer.
        *     text: 'Cancel',
        *     type: 'button-default',
        *     onTap: function(e) {
        *       // e.preventDefault() will stop the popup from closing when tapped.
        *       e.preventDefault();
        *     }
        *   }, {
        *     text: 'OK',
        *     type: 'button-positive',
        *     onTap: function(e) {
        *       // Returning a value will cause the promise to resolve with the given value.
        *       return scope.data.response;
        *     }
        *   }]
        * }
        * ```
        *
        * @returns {object} A promise which is resolved when the popup is closed. Has an additional
        * `close` function, which can be used to programmatically close the popup.
        */
        show: showPopup,

        /**
        * @ngdoc method
        * @name $ionicPopup#alert
        * @description Show a simple alert popup with a message and one button that the user can
        * tap to close the popup.
        *
        * @param {object} options The options for showing the alert, of the form:
        *
        * ```
        * {
        *   title: '', // String. The title of the popup.
        *   cssClass: '', // String, The custom CSS class name
        *   subTitle: '', // String (optional). The sub-title of the popup.
        *   template: '', // String (optional). The html template to place in the popup body.
        *   templateUrl: '', // String (optional). The URL of an html template to place in the popup   body.
        *   okText: '', // String (default: 'OK'). The text of the OK button.
        *   okType: '', // String (default: 'button-positive'). The type of the OK button.
        * }
        * ```
        *
        * @returns {object} A promise which is resolved when the popup is closed. Has one additional
        * function `close`, which can be called with any value to programmatically close the popup
        * with the given value.
        */
        alert: showAlert,

        /**
        * @ngdoc method
        * @name $ionicPopup#confirm
        * @description
        * Show a simple confirm popup with a Cancel and OK button.
        *
        * Resolves the promise with true if the user presses the OK button, and false if the
        * user presses the Cancel button.
        *
        * @param {object} options The options for showing the confirm popup, of the form:
        *
        * ```
        * {
        *   title: '', // String. The title of the popup.
        *   cssClass: '', // String, The custom CSS class name
        *   subTitle: '', // String (optional). The sub-title of the popup.
        *   template: '', // String (optional). The html template to place in the popup body.
        *   templateUrl: '', // String (optional). The URL of an html template to place in the popup   body.
        *   cancelText: '', // String (default: 'Cancel'). The text of the Cancel button.
        *   cancelType: '', // String (default: 'button-default'). The type of the Cancel button.
        *   okText: '', // String (default: 'OK'). The text of the OK button.
        *   okType: '', // String (default: 'button-positive'). The type of the OK button.
        * }
        * ```
        *
        * @returns {object} A promise which is resolved when the popup is closed. Has one additional
        * function `close`, which can be called with any value to programmatically close the popup
        * with the given value.
        */
        confirm: showConfirm,

        /**
        * @ngdoc method
        * @name $ionicPopup#prompt
        * @description Show a simple prompt popup, which has an input, OK button, and Cancel button.
        * Resolves the promise with the value of the input if the user presses OK, and with undefined
        * if the user presses Cancel.
        *
        * ```javascript
        *  $ionicPopup.prompt({
        *    title: 'Password Check',
        *    template: 'Enter your secret password',
        *    inputType: 'password',
        *    inputPlaceholder: 'Your password'
        *  }).then(function(res) {
        *    console.log('Your password is', res);
        *  });
        * ```
        * @param {object} options The options for showing the prompt popup, of the form:
        *
        * ```
        * {
        *   title: '', // String. The title of the popup.
        *   cssClass: '', // String, The custom CSS class name
        *   subTitle: '', // String (optional). The sub-title of the popup.
        *   template: '', // String (optional). The html template to place in the popup body.
        *   templateUrl: '', // String (optional). The URL of an html template to place in the popup body.
        *   inputType: // String (default: 'text'). The type of input to use
        *   defaultText: // String (default: ''). The initial value placed into the input.
        *   maxLength: // Integer (default: null). Specify a maxlength attribute for the input.
        *   inputPlaceholder: // String (default: ''). A placeholder to use for the input.
        *   cancelText: // String (default: 'Cancel'. The text of the Cancel button.
        *   cancelType: // String (default: 'button-default'). The type of the Cancel button.
        *   okText: // String (default: 'OK'). The text of the OK button.
        *   okType: // String (default: 'button-positive'). The type of the OK button.
        * }
        * ```
        *
        * @returns {object} A promise which is resolved when the popup is closed. Has one additional
        * function `close`, which can be called with any value to programmatically close the popup
        * with the given value.
        */
        prompt: showPrompt,
        /**
        * @private for testing
        */
        _createPopup: createPopup,
        _popupStack: popupStack
    };

    return $ionicPopup;

    function createPopup(options) {
        options = extend({
            scope: null,
            title: '',
            buttons: []
        }, options || {});

        var self = {};
        self.scope = (options.scope || $rootScope).$new();
        self.element = jqLite(POPUP_TPL);
        self.responseDeferred = $q.defer();

        $ionicBody.get().appendChild(self.element[0]);
        $compile(self.element)(self.scope);

        extend(self.scope, {
            title: options.title,
            buttons: options.buttons,
            subTitle: options.subTitle,
            cssClass: options.cssClass,
            $buttonTapped: function (button, event) {
                var result = (button.onTap || noop).apply(self, [event]);
                event = event.originalEvent || event; //jquery events

                if (!event.defaultPrevented) {
                    self.responseDeferred.resolve(result);
                }
            }
        });

        $q.when(
      options.templateUrl ?
      $ionicTemplateLoader.load(options.templateUrl) :
        (options.template || options.content || '')
    ).then(function (template) {
        var popupBody = jqLite(self.element[0].querySelector('.popup-body'));
        if (template) {
            popupBody.html(template);
            $compile(popupBody.contents())(self.scope);
        } else {
            popupBody.remove();
        }
    });

        self.show = function () {
            if (self.isShown || self.removed) return;

            $ionicModal.stack.add(self);
            self.isShown = true;
            ionic.requestAnimationFrame(function () {
                //if hidden while waiting for raf, don't show
                if (!self.isShown) return;

                self.element.removeClass('popup-hidden');
                self.element.addClass('popup-showing active');
                focusInput(self.element);
            });
        };

        self.hide = function (callback) {
            callback = callback || noop;
            if (!self.isShown) return callback();

            $ionicModal.stack.remove(self);
            self.isShown = false;
            self.element.removeClass('active');
            self.element.addClass('popup-hidden');
            $timeout(callback, 250, false);
        };

        self.remove = function () {
            if (self.removed) return;

            self.hide(function () {
                self.element.remove();
                self.scope.$destroy();
            });

            self.removed = true;
        };

        return self;
    }

    function onHardwareBackButton() {
        var last = popupStack[popupStack.length - 1];
        last && last.responseDeferred.resolve();
    }

    function showPopup(options) {
        var popup = $ionicPopup._createPopup(options);
        var showDelay = 0;

        if (popupStack.length > 0) {
            showDelay = config.stackPushDelay;
            $timeout(popupStack[popupStack.length - 1].hide, showDelay, false);
        } else {
            //Add popup-open & backdrop if this is first popup
            $ionicBody.addClass('popup-open');
            $ionicBackdrop.retain();
            //only show the backdrop on the first popup
            $ionicPopup._backButtonActionDone = $ionicPlatform.registerBackButtonAction(
        onHardwareBackButton,
        IONIC_BACK_PRIORITY.popup
      );
        }

        // Expose a 'close' method on the returned promise
        popup.responseDeferred.promise.close = function popupClose(result) {
            if (!popup.removed) popup.responseDeferred.resolve(result);
        };
        //DEPRECATED: notify the promise with an object with a close method
        popup.responseDeferred.notify({ close: popup.responseDeferred.close });

        doShow();

        return popup.responseDeferred.promise;

        function doShow() {
            popupStack.push(popup);
            $timeout(popup.show, showDelay, false);

            popup.responseDeferred.promise.then(function (result) {
                var index = popupStack.indexOf(popup);
                if (index !== -1) {
                    popupStack.splice(index, 1);
                }

                popup.remove();

                if (popupStack.length > 0) {
                    popupStack[popupStack.length - 1].show();
                } else {
                    $ionicBackdrop.release();
                    //Remove popup-open & backdrop if this is last popup
                    $timeout(function () {
                        // wait to remove this due to a 300ms delay native
                        // click which would trigging whatever was underneath this
                        if (!popupStack.length) {
                            $ionicBody.removeClass('popup-open');
                        }
                    }, 400, false);
                    ($ionicPopup._backButtonActionDone || noop)();
                }


                return result;
            });

        }

    }

    function focusInput(element) {
        var focusOn = element[0].querySelector('[autofocus]');
        if (focusOn) {
            focusOn.focus();
        }
    }

    function showAlert(opts) {
        return showPopup(extend({
            buttons: [{
                text: opts.okText || 'OK',
                type: opts.okType || 'button-positive',
                onTap: function () {
                    return true;
                }
            }]
        }, opts || {}));
    }

    function showConfirm(opts) {
        return showPopup(extend({
            buttons: [{
                text: opts.cancelText || 'Cancel',
                type: opts.cancelType || 'button-default',
                onTap: function () { return false; }
            }, {
                text: opts.okText || 'OK',
                type: opts.okType || 'button-positive',
                onTap: function () { return true; }
            }]
        }, opts || {}));
    }

    function showPrompt(opts) {
        var scope = $rootScope.$new(true);
        scope.data = {};
        scope.data.fieldtype = opts.inputType ? opts.inputType : 'text';
        scope.data.response = opts.defaultText ? opts.defaultText : '';
        scope.data.placeholder = opts.inputPlaceholder ? opts.inputPlaceholder : '';
        scope.data.maxlength = opts.maxLength ? parseInt(opts.maxLength) : '';
        var text = '';
        if (opts.template && /<[a-z][\s\S]*>/i.test(opts.template) === false) {
            text = '<span>' + opts.template + '</span>';
            delete opts.template;
        }
        return showPopup(extend({
            template: text + '<input ng-model="data.response" '
        + 'type="{{ data.fieldtype }}"'
        + 'maxlength="{{ data.maxlength }}"'
        + 'placeholder="{{ data.placeholder }}"'
        + '>',
            scope: scope,
            buttons: [{
                text: opts.cancelText || 'Cancel',
                type: opts.cancelType || 'button-default',
                onTap: function () { }
            }, {
                text: opts.okText || 'OK',
                type: opts.okType || 'button-positive',
                onTap: function () {
                    return scope.data.response || '';
                }
            }]
        }, opts || {}));
    }
} ]);

    /**
    * @ngdoc service
    * @name $ionicPosition
    * @module ionic
    * @description
    * A set of utility methods that can be use to retrieve position of DOM elements.
    * It is meant to be used where we need to absolute-position DOM elements in
    * relation to other, existing elements (this is the case for tooltips, popovers, etc.).
    *
    * Adapted from [AngularUI Bootstrap](https://github.com/angular-ui/bootstrap/blob/master/src/position/position.js),
    * ([license](https://github.com/angular-ui/bootstrap/blob/master/LICENSE))
    */
    IonicModule
.factory('$ionicPosition', ['$document', '$window', function ($document, $window) {

    function getStyle(el, cssprop) {
        if (el.currentStyle) { //IE
            return el.currentStyle[cssprop];
        } else if ($window.getComputedStyle) {
            return $window.getComputedStyle(el)[cssprop];
        }
        // finally try and get inline style
        return el.style[cssprop];
    }

    /**
    * Checks if a given element is statically positioned
    * @param element - raw DOM element
    */
    function isStaticPositioned(element) {
        return (getStyle(element, 'position') || 'static') === 'static';
    }

    /**
    * returns the closest, non-statically positioned parentOffset of a given element
    * @param element
    */
    var parentOffsetEl = function (element) {
        var docDomEl = $document[0];
        var offsetParent = element.offsetParent || docDomEl;
        while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent)) {
            offsetParent = offsetParent.offsetParent;
        }
        return offsetParent || docDomEl;
    };

    return {
        /**
        * @ngdoc method
        * @name $ionicPosition#position
        * @description Get the current coordinates of the element, relative to the offset parent.
        * Read-only equivalent of [jQuery's position function](http://api.jquery.com/position/).
        * @param {element} element The element to get the position of.
        * @returns {object} Returns an object containing the properties top, left, width and height.
        */
        position: function (element) {
            var elBCR = this.offset(element);
            var offsetParentBCR = { top: 0, left: 0 };
            var offsetParentEl = parentOffsetEl(element[0]);
            if (offsetParentEl != $document[0]) {
                offsetParentBCR = this.offset(jqLite(offsetParentEl));
                offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop;
                offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft;
            }

            var boundingClientRect = element[0].getBoundingClientRect();
            return {
                width: boundingClientRect.width || element.prop('offsetWidth'),
                height: boundingClientRect.height || element.prop('offsetHeight'),
                top: elBCR.top - offsetParentBCR.top,
                left: elBCR.left - offsetParentBCR.left
            };
        },

        /**
        * @ngdoc method
        * @name $ionicPosition#offset
        * @description Get the current coordinates of the element, relative to the document.
        * Read-only equivalent of [jQuery's offset function](http://api.jquery.com/offset/).
        * @param {element} element The element to get the offset of.
        * @returns {object} Returns an object containing the properties top, left, width and height.
        */
        offset: function (element) {
            var boundingClientRect = element[0].getBoundingClientRect();
            return {
                width: boundingClientRect.width || element.prop('offsetWidth'),
                height: boundingClientRect.height || element.prop('offsetHeight'),
                top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop),
                left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)
            };
        }

    };
} ]);


    /**
    * @ngdoc service
    * @name $ionicScrollDelegate
    * @module ionic
    * @description
    * Delegate for controlling scrollViews (created by
    * {@link ionic.directive:ionContent} and
    * {@link ionic.directive:ionScroll} directives).
    *
    * Methods called directly on the $ionicScrollDelegate service will control all scroll
    * views.  Use the {@link ionic.service:$ionicScrollDelegate#$getByHandle $getByHandle}
    * method to control specific scrollViews.
    *
    * @usage
    *
    * ```html
    * <body ng-controller="MainCtrl">
    *   <ion-content>
    *     <button ng-click="scrollTop()">Scroll to Top!</button>
    *   </ion-content>
    * </body>
    * ```
    * ```js
    * function MainCtrl($scope, $ionicScrollDelegate) {
    *   $scope.scrollTop = function() {
    *     $ionicScrollDelegate.scrollTop();
    *   };
    * }
    * ```
    *
    * Example of advanced usage, with two scroll areas using `delegate-handle`
    * for fine control.
    *
    * ```html
    * <body ng-controller="MainCtrl">
    *   <ion-content delegate-handle="mainScroll">
    *     <button ng-click="scrollMainToTop()">
    *       Scroll content to top!
    *     </button>
    *     <ion-scroll delegate-handle="small" style="height: 100px;">
    *       <button ng-click="scrollSmallToTop()">
    *         Scroll small area to top!
    *       </button>
    *     </ion-scroll>
    *   </ion-content>
    * </body>
    * ```
    * ```js
    * function MainCtrl($scope, $ionicScrollDelegate) {
    *   $scope.scrollMainToTop = function() {
    *     $ionicScrollDelegate.$getByHandle('mainScroll').scrollTop();
    *   };
    *   $scope.scrollSmallToTop = function() {
    *     $ionicScrollDelegate.$getByHandle('small').scrollTop();
    *   };
    * }
    * ```
    */
    IonicModule
.service('$ionicScrollDelegate', ionic.DelegateService([
    /**
    * @ngdoc method
    * @name $ionicScrollDelegate#resize
    * @description Tell the scrollView to recalculate the size of its container.
    */
  'resize',
    /**
    * @ngdoc method
    * @name $ionicScrollDelegate#scrollTop
    * @param {boolean=} shouldAnimate Whether the scroll should animate.
    */
  'scrollTop',
    /**
    * @ngdoc method
    * @name $ionicScrollDelegate#scrollBottom
    * @param {boolean=} shouldAnimate Whether the scroll should animate.
    */
  'scrollBottom',
    /**
    * @ngdoc method
    * @name $ionicScrollDelegate#scrollTo
    * @param {number} left The x-value to scroll to.
    * @param {number} top The y-value to scroll to.
    * @param {boolean=} shouldAnimate Whether the scroll should animate.
    */
  'scrollTo',
    /**
    * @ngdoc method
    * @name $ionicScrollDelegate#scrollBy
    * @param {number} left The x-offset to scroll by.
    * @param {number} top The y-offset to scroll by.
    * @param {boolean=} shouldAnimate Whether the scroll should animate.
    */
  'scrollBy',
    /**
    * @ngdoc method
    * @name $ionicScrollDelegate#zoomTo
    * @param {number} level Level to zoom to.
    * @param {boolean=} animate Whether to animate the zoom.
    * @param {number=} originLeft Zoom in at given left coordinate.
    * @param {number=} originTop Zoom in at given top coordinate.
    */
  'zoomTo',
    /**
    * @ngdoc method
    * @name $ionicScrollDelegate#zoomBy
    * @param {number} factor The factor to zoom by.
    * @param {boolean=} animate Whether to animate the zoom.
    * @param {number=} originLeft Zoom in at given left coordinate.
    * @param {number=} originTop Zoom in at given top coordinate.
    */
  'zoomBy',
    /**
    * @ngdoc method
    * @name $ionicScrollDelegate#getScrollPosition
    * @returns {object} The scroll position of this view, with the following properties:
    *  - `{number}` `left` The distance the user has scrolled from the left (starts at 0).
    *  - `{number}` `top` The distance the user has scrolled from the top (starts at 0).
    *  - `{number}` `zoom` The current zoom level.
    */
  'getScrollPosition',
    /**
    * @ngdoc method
    * @name $ionicScrollDelegate#anchorScroll
    * @description Tell the scrollView to scroll to the element with an id
    * matching window.location.hash.
    *
    * If no matching element is found, it will scroll to top.
    *
    * @param {boolean=} shouldAnimate Whether the scroll should animate.
    */
  'anchorScroll',
    /**
    * @ngdoc method
    * @name $ionicScrollDelegate#freezeScroll
    * @description Does not allow this scroll view to scroll either x or y.
    * @param {boolean=} shouldFreeze Should this scroll view be prevented from scrolling or not.
    * @returns {boolean} If the scroll view is being prevented from scrolling or not.
    */
  'freezeScroll',
    /**
    * @ngdoc method
    * @name $ionicScrollDelegate#freezeAllScrolls
    * @description Does not allow any of the app's scroll views to scroll either x or y.
    * @param {boolean=} shouldFreeze Should all app scrolls be prevented from scrolling or not.
    */
  'freezeAllScrolls',
    /**
    * @ngdoc method
    * @name $ionicScrollDelegate#getScrollView
    * @returns {object} The scrollView associated with this delegate.
    */
  'getScrollView'
    /**
    * @ngdoc method
    * @name $ionicScrollDelegate#$getByHandle
    * @param {string} handle
    * @returns `delegateInstance` A delegate instance that controls only the
    * scrollViews with `delegate-handle` matching the given handle.
    *
    * Example: `$ionicScrollDelegate.$getByHandle('my-handle').scrollTop();`
    */
]));


    /**
    * @ngdoc service
    * @name $ionicSideMenuDelegate
    * @module ionic
    *
    * @description
    * Delegate for controlling the {@link ionic.directive:ionSideMenus} directive.
    *
    * Methods called directly on the $ionicSideMenuDelegate service will control all side
    * menus.  Use the {@link ionic.service:$ionicSideMenuDelegate#$getByHandle $getByHandle}
    * method to control specific ionSideMenus instances.
    *
    * @usage
    *
    * ```html
    * <body ng-controller="MainCtrl">
    *   <ion-side-menus>
    *     <ion-side-menu-content>
    *       Content!
    *       <button ng-click="toggleLeftSideMenu()">
    *         Toggle Left Side Menu
    *       </button>
    *     </ion-side-menu-content>
    *     <ion-side-menu side="left">
    *       Left Menu!
    *     <ion-side-menu>
    *   </ion-side-menus>
    * </body>
    * ```
    * ```js
    * function MainCtrl($scope, $ionicSideMenuDelegate) {
    *   $scope.toggleLeftSideMenu = function() {
    *     $ionicSideMenuDelegate.toggleLeft();
    *   };
    * }
    * ```
    */
    IonicModule
.service('$ionicSideMenuDelegate', ionic.DelegateService([
    /**
    * @ngdoc method
    * @name $ionicSideMenuDelegate#toggleLeft
    * @description Toggle the left side menu (if it exists).
    * @param {boolean=} isOpen Whether to open or close the menu.
    * Default: Toggles the menu.
    */
  'toggleLeft',
    /**
    * @ngdoc method
    * @name $ionicSideMenuDelegate#toggleRight
    * @description Toggle the right side menu (if it exists).
    * @param {boolean=} isOpen Whether to open or close the menu.
    * Default: Toggles the menu.
    */
  'toggleRight',
    /**
    * @ngdoc method
    * @name $ionicSideMenuDelegate#getOpenRatio
    * @description Gets the ratio of open amount over menu width. For example, a
    * menu of width 100 that is opened by 50 pixels is 50% opened, and would return
    * a ratio of 0.5.
    *
    * @returns {float} 0 if nothing is open, between 0 and 1 if left menu is
    * opened/opening, and between 0 and -1 if right menu is opened/opening.
    */
  'getOpenRatio',
    /**
    * @ngdoc method
    * @name $ionicSideMenuDelegate#isOpen
    * @returns {boolean} Whether either the left or right menu is currently opened.
    */
  'isOpen',
    /**
    * @ngdoc method
    * @name $ionicSideMenuDelegate#isOpenLeft
    * @returns {boolean} Whether the left menu is currently opened.
    */
  'isOpenLeft',
    /**
    * @ngdoc method
    * @name $ionicSideMenuDelegate#isOpenRight
    * @returns {boolean} Whether the right menu is currently opened.
    */
  'isOpenRight',
    /**
    * @ngdoc method
    * @name $ionicSideMenuDelegate#canDragContent
    * @param {boolean=} canDrag Set whether the content can or cannot be dragged to open
    * side menus.
    * @returns {boolean} Whether the content can be dragged to open side menus.
    */
  'canDragContent',
    /**
    * @ngdoc method
    * @name $ionicSideMenuDelegate#edgeDragThreshold
    * @param {boolean|number=} value Set whether the content drag can only start if it is below a certain threshold distance from the edge of the screen. Accepts three different values:
    *  - If a non-zero number is given, that many pixels is used as the maximum allowed distance from the edge that starts dragging the side menu.
    *  - If true is given, the default number of pixels (25) is used as the maximum allowed distance.
    *  - If false or 0 is given, the edge drag threshold is disabled, and dragging from anywhere on the content is allowed.
    * @returns {boolean} Whether the drag can start only from within the edge of screen threshold.
    */
  'edgeDragThreshold'
    /**
    * @ngdoc method
    * @name $ionicSideMenuDelegate#$getByHandle
    * @param {string} handle
    * @returns `delegateInstance` A delegate instance that controls only the
    * {@link ionic.directive:ionSideMenus} directives with `delegate-handle` matching
    * the given handle.
    *
    * Example: `$ionicSideMenuDelegate.$getByHandle('my-handle').toggleLeft();`
    */
]));


    /**
    * @ngdoc service
    * @name $ionicSlideBoxDelegate
    * @module ionic
    * @description
    * Delegate that controls the {@link ionic.directive:ionSlideBox} directive.
    *
    * Methods called directly on the $ionicSlideBoxDelegate service will control all slide boxes.  Use the {@link ionic.service:$ionicSlideBoxDelegate#$getByHandle $getByHandle}
    * method to control specific slide box instances.
    *
    * @usage
    *
    * ```html
    * <ion-view>
    *   <ion-slide-box>
    *     <ion-slide>
    *       <div class="box blue">
    *         <button ng-click="nextSlide()">Next slide!</button>
    *       </div>
    *     </ion-slide>
    *     <ion-slide>
    *       <div class="box red">
    *         Slide 2!
    *       </div>
    *     </ion-slide>
    *   </ion-slide-box>
    * </ion-view>
    * ```
    * ```js
    * function MyCtrl($scope, $ionicSlideBoxDelegate) {
    *   $scope.nextSlide = function() {
    *     $ionicSlideBoxDelegate.next();
    *   }
    * }
    * ```
    */
    IonicModule
.service('$ionicSlideBoxDelegate', ionic.DelegateService([
    /**
    * @ngdoc method
    * @name $ionicSlideBoxDelegate#update
    * @description
    * Update the slidebox (for example if using Angular with ng-repeat,
    * resize it for the elements inside).
    */
  'update',
    /**
    * @ngdoc method
    * @name $ionicSlideBoxDelegate#slide
    * @param {number} to The index to slide to.
    * @param {number=} speed The number of milliseconds the change should take.
    */
  'slide',
  'select',
    /**
    * @ngdoc method
    * @name $ionicSlideBoxDelegate#enableSlide
    * @param {boolean=} shouldEnable Whether to enable sliding the slidebox.
    * @returns {boolean} Whether sliding is enabled.
    */
  'enableSlide',
    /**
    * @ngdoc method
    * @name $ionicSlideBoxDelegate#previous
    * @param {number=} speed The number of milliseconds the change should take.
    * @description Go to the previous slide. Wraps around if at the beginning.
    */
  'previous',
    /**
    * @ngdoc method
    * @name $ionicSlideBoxDelegate#next
    * @param {number=} speed The number of milliseconds the change should take.
    * @description Go to the next slide. Wraps around if at the end.
    */
  'next',
    /**
    * @ngdoc method
    * @name $ionicSlideBoxDelegate#stop
    * @description Stop sliding. The slideBox will not move again until
    * explicitly told to do so.
    */
  'stop',
  'autoPlay',
    /**
    * @ngdoc method
    * @name $ionicSlideBoxDelegate#start
    * @description Start sliding again if the slideBox was stopped.
    */
  'start',
    /**
    * @ngdoc method
    * @name $ionicSlideBoxDelegate#currentIndex
    * @returns number The index of the current slide.
    */
  'currentIndex',
  'selected',
    /**
    * @ngdoc method
    * @name $ionicSlideBoxDelegate#slidesCount
    * @returns number The number of slides there are currently.
    */
  'slidesCount',
  'count',
  'loop'
    /**
    * @ngdoc method
    * @name $ionicSlideBoxDelegate#$getByHandle
    * @param {string} handle
    * @returns `delegateInstance` A delegate instance that controls only the
    * {@link ionic.directive:ionSlideBox} directives with `delegate-handle` matching
    * the given handle.
    *
    * Example: `$ionicSlideBoxDelegate.$getByHandle('my-handle').stop();`
    */
]));


    /**
    * @ngdoc service
    * @name $ionicTabsDelegate
    * @module ionic
    *
    * @description
    * Delegate for controlling the {@link ionic.directive:ionTabs} directive.
    *
    * Methods called directly on the $ionicTabsDelegate service will control all ionTabs
    * directives. Use the {@link ionic.service:$ionicTabsDelegate#$getByHandle $getByHandle}
    * method to control specific ionTabs instances.
    *
    * @usage
    *
    * ```html
    * <body ng-controller="MyCtrl">
    *   <ion-tabs>
    *
    *     <ion-tab title="Tab 1">
    *       Hello tab 1!
    *       <button ng-click="selectTabWithIndex(1)">Select tab 2!</button>
    *     </ion-tab>
    *     <ion-tab title="Tab 2">Hello tab 2!</ion-tab>
    *
    *   </ion-tabs>
    * </body>
    * ```
    * ```js
    * function MyCtrl($scope, $ionicTabsDelegate) {
    *   $scope.selectTabWithIndex = function(index) {
    *     $ionicTabsDelegate.select(index);
    *   }
    * }
    * ```
    */
    IonicModule
.service('$ionicTabsDelegate', ionic.DelegateService([
    /**
    * @ngdoc method
    * @name $ionicTabsDelegate#select
    * @description Select the tab matching the given index.
    *
    * @param {number} index Index of the tab to select.
    */
  'select',
    /**
    * @ngdoc method
    * @name $ionicTabsDelegate#selectedIndex
    * @returns `number` The index of the selected tab, or -1.
    */
  'selectedIndex',
    /**
    * @ngdoc method
    * @name $ionicTabsDelegate#showBar
    * @description
    * Set/get whether the {@link ionic.directive:ionTabs} is shown
    * @param {boolean} show Whether to show the bar.
    * @returns {boolean} Whether the bar is shown.
    */
  'showBar'
    /**
    * @ngdoc method
    * @name $ionicTabsDelegate#$getByHandle
    * @param {string} handle
    * @returns `delegateInstance` A delegate instance that controls only the
    * {@link ionic.directive:ionTabs} directives with `delegate-handle` matching
    * the given handle.
    *
    * Example: `$ionicTabsDelegate.$getByHandle('my-handle').select(0);`
    */
]));

    // closure to keep things neat
    (function () {
        var templatesToCache = [];

        /**
        * @ngdoc service
        * @name $ionicTemplateCache
        * @module ionic
        * @description A service that preemptively caches template files to eliminate transition flicker and boost performance.
        * @usage
        * State templates are cached automatically, but you can optionally cache other templates.
        *
        * ```js
        * $ionicTemplateCache('myNgIncludeTemplate.html');
        * ```
        *
        * Optionally disable all preemptive caching with the `$ionicConfigProvider` or individual states by setting `prefetchTemplate`
        * in the `$state` definition
        *
        * ```js
        *   angular.module('myApp', ['ionic'])
        *   .config(function($stateProvider, $ionicConfigProvider) {
        *
        *     // disable preemptive template caching globally
        *     $ionicConfigProvider.templates.prefetch(false);
        *
        *     // disable individual states
        *     $stateProvider
        *       .state('tabs', {
        *         url: "/tab",
        *         abstract: true,
        *         prefetchTemplate: false,
        *         templateUrl: "tabs-templates/tabs.html"
        *       })
        *       .state('tabs.home', {
        *         url: "/home",
        *         views: {
        *           'home-tab': {
        *             prefetchTemplate: false,
        *             templateUrl: "tabs-templates/home.html",
        *             controller: 'HomeTabCtrl'
        *           }
        *         }
        *       });
        *   });
        * ```
        */
        IonicModule
.factory('$ionicTemplateCache', [
'$http',
'$templateCache',
'$timeout',
function ($http, $templateCache, $timeout) {
    var toCache = templatesToCache,
      hasRun;

    function $ionicTemplateCache(templates) {
        if (typeof templates === 'undefined') {
            return run();
        }
        if (isString(templates)) {
            templates = [templates];
        }
        forEach(templates, function (template) {
            toCache.push(template);
        });
        if (hasRun) {
            run();
        }
    }

    // run through methods - internal method
    function run() {
        var template;
        $ionicTemplateCache._runCount++;

        hasRun = true;
        // ignore if race condition already zeroed out array
        if (toCache.length === 0) return;

        var i = 0;
        while (i < 4 && (template = toCache.pop())) {
            // note that inline templates are ignored by this request
            if (isString(template)) $http.get(template, { cache: $templateCache });
            i++;
        }
        // only preload 3 templates a second
        if (toCache.length) {
            $timeout(run, 1000);
        }
    }

    // exposing for testing
    $ionicTemplateCache._runCount = 0;
    // default method
    return $ionicTemplateCache;
} ])

        // Intercepts the $stateprovider.state() command to look for templateUrls that can be cached
.config([
'$stateProvider',
'$ionicConfigProvider',
function ($stateProvider, $ionicConfigProvider) {
    var stateProviderState = $stateProvider.state;
    $stateProvider.state = function (stateName, definition) {
        // don't even bother if it's disabled. note, another config may run after this, so it's not a catch-all
        if (typeof definition === 'object') {
            var enabled = definition.prefetchTemplate !== false && templatesToCache.length < $ionicConfigProvider.templates.maxPrefetch();
            if (enabled && isString(definition.templateUrl)) templatesToCache.push(definition.templateUrl);
            if (angular.isObject(definition.views)) {
                for (var key in definition.views) {
                    enabled = definition.views[key].prefetchTemplate !== false && templatesToCache.length < $ionicConfigProvider.templates.maxPrefetch();
                    if (enabled && isString(definition.views[key].templateUrl)) templatesToCache.push(definition.views[key].templateUrl);
                }
            }
        }
        return stateProviderState.call($stateProvider, stateName, definition);
    };
} ])

        // process the templateUrls collected by the $stateProvider, adding them to the cache
.run(['$ionicTemplateCache', function ($ionicTemplateCache) {
    $ionicTemplateCache();
} ]);

    })();

    IonicModule
.factory('$ionicTemplateLoader', [
  '$compile',
  '$controller',
  '$http',
  '$q',
  '$rootScope',
  '$templateCache',
function ($compile, $controller, $http, $q, $rootScope, $templateCache) {

    return {
        load: fetchTemplate,
        compile: loadAndCompile
    };

    function fetchTemplate(url) {
        return $http.get(url, { cache: $templateCache })
    .then(function (response) {
        return response.data && response.data.trim();
    });
    }

    function loadAndCompile(options) {
        options = extend({
            template: '',
            templateUrl: '',
            scope: null,
            controller: null,
            locals: {},
            appendTo: null
        }, options || {});

        var templatePromise = options.templateUrl ?
      this.load(options.templateUrl) :
      $q.when(options.template);

        return templatePromise.then(function (template) {
            var controller;
            var scope = options.scope || $rootScope.$new();

            //Incase template doesn't have just one root element, do this
            var element = jqLite('<div>').html(template).contents();

            if (options.controller) {
                controller = $controller(
          options.controller,
          extend(options.locals, {
              $scope: scope
          })
        );
                element.children().data('$ngControllerController', controller);
            }
            if (options.appendTo) {
                jqLite(options.appendTo).append(element);
            }

            $compile(element)(scope);

            return {
                element: element,
                scope: scope
            };
        });
    }

} ]);

    /**
    * @private
    * DEPRECATED, as of v1.0.0-beta14 -------
    */
    IonicModule
.factory('$ionicViewService', ['$ionicHistory', '$log', function ($ionicHistory, $log) {

    function warn(oldMethod, newMethod) {
        $log.warn('$ionicViewService' + oldMethod + ' is deprecated, please use $ionicHistory' + newMethod + ' instead: http://ionicframework.com/docs/nightly/api/service/$ionicHistory/');
    }

    warn('', '');

    var methodsMap = {
        getCurrentView: 'currentView',
        getBackView: 'backView',
        getForwardView: 'forwardView',
        getCurrentStateName: 'currentStateName',
        nextViewOptions: 'nextViewOptions',
        clearHistory: 'clearHistory'
    };

    forEach(methodsMap, function (newMethod, oldMethod) {
        methodsMap[oldMethod] = function () {
            warn('.' + oldMethod, '.' + newMethod);
            return $ionicHistory[newMethod].apply(this, arguments);
        };
    });

    return methodsMap;

} ]);

    /**
    * @private
    * TODO document
    */

    IonicModule.factory('$ionicViewSwitcher', [
  '$timeout',
  '$document',
  '$q',
  '$ionicClickBlock',
  '$ionicConfig',
  '$ionicNavBarDelegate',
function ($timeout, $document, $q, $ionicClickBlock, $ionicConfig, $ionicNavBarDelegate) {

    var TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend';
    var DATA_NO_CACHE = '$noCache';
    var DATA_DESTROY_ELE = '$destroyEle';
    var DATA_ELE_IDENTIFIER = '$eleId';
    var DATA_VIEW_ACCESSED = '$accessed';
    var DATA_FALLBACK_TIMER = '$fallbackTimer';
    var DATA_VIEW = '$viewData';
    var NAV_VIEW_ATTR = 'nav-view';
    var VIEW_STATUS_ACTIVE = 'active';
    var VIEW_STATUS_CACHED = 'cached';
    var VIEW_STATUS_STAGED = 'stage';

    var transitionCounter = 0;
    var nextTransition, nextDirection;
    ionic.transition = ionic.transition || {};
    ionic.transition.isActive = false;
    var isActiveTimer;
    var cachedAttr = ionic.DomUtil.cachedAttr;
    var transitionPromises = [];
    var defaultTimeout = 1100;

    var ionicViewSwitcher = {

        create: function (navViewCtrl, viewLocals, enteringView, leavingView, renderStart, renderEnd) {
            // get a reference to an entering/leaving element if they exist
            // loop through to see if the view is already in the navViewElement
            var enteringEle, leavingEle;
            var transitionId = ++transitionCounter;
            var alreadyInDom;

            var switcher = {

                init: function (registerData, callback) {
                    ionicViewSwitcher.isTransitioning(true);

                    switcher.loadViewElements(registerData);

                    switcher.render(registerData, function () {
                        callback && callback();
                    });
                },

                loadViewElements: function (registerData) {
                    var x, l, viewEle;
                    var viewElements = navViewCtrl.getViewElements();
                    var enteringEleIdentifier = getViewElementIdentifier(viewLocals, enteringView);
                    var navViewActiveEleId = navViewCtrl.activeEleId();

                    for (x = 0, l = viewElements.length; x < l; x++) {
                        viewEle = viewElements.eq(x);

                        if (viewEle.data(DATA_ELE_IDENTIFIER) === enteringEleIdentifier) {
                            // we found an existing element in the DOM that should be entering the view
                            if (viewEle.data(DATA_NO_CACHE)) {
                                // the existing element should not be cached, don't use it
                                viewEle.data(DATA_ELE_IDENTIFIER, enteringEleIdentifier + ionic.Utils.nextUid());
                                viewEle.data(DATA_DESTROY_ELE, true);

                            } else {
                                enteringEle = viewEle;
                            }

                        } else if (isDefined(navViewActiveEleId) && viewEle.data(DATA_ELE_IDENTIFIER) === navViewActiveEleId) {
                            leavingEle = viewEle;
                        }

                        if (enteringEle && leavingEle) break;
                    }

                    alreadyInDom = !!enteringEle;

                    if (!alreadyInDom) {
                        // still no existing element to use
                        // create it using existing template/scope/locals
                        enteringEle = registerData.ele || ionicViewSwitcher.createViewEle(viewLocals);

                        // existing elements in the DOM are looked up by their state name and state id
                        enteringEle.data(DATA_ELE_IDENTIFIER, enteringEleIdentifier);
                    }

                    if (renderEnd) {
                        navViewCtrl.activeEleId(enteringEleIdentifier);
                    }

                    registerData.ele = null;
                },

                render: function (registerData, callback) {
                    if (alreadyInDom) {
                        // it was already found in the DOM, just reconnect the scope
                        ionic.Utils.reconnectScope(enteringEle.scope());

                    } else {
                        // the entering element is not already in the DOM
                        // set that the entering element should be "staged" and its
                        // styles of where this element will go before it hits the DOM
                        navViewAttr(enteringEle, VIEW_STATUS_STAGED);

                        var enteringData = getTransitionData(viewLocals, enteringEle, registerData.direction, enteringView);
                        var transitionFn = $ionicConfig.transitions.views[enteringData.transition] || $ionicConfig.transitions.views.none;
                        transitionFn(enteringEle, null, enteringData.direction, true).run(0);

                        enteringEle.data(DATA_VIEW, {
                            viewId: enteringData.viewId,
                            historyId: enteringData.historyId,
                            stateName: enteringData.stateName,
                            stateParams: enteringData.stateParams
                        });

                        // if the current state has cache:false
                        // or the element has cache-view="false" attribute
                        if (viewState(viewLocals).cache === false || viewState(viewLocals).cache === 'false' ||
                enteringEle.attr('cache-view') == 'false' || $ionicConfig.views.maxCache() === 0) {
                            enteringEle.data(DATA_NO_CACHE, true);
                        }

                        // append the entering element to the DOM, create a new scope and run link
                        var viewScope = navViewCtrl.appendViewElement(enteringEle, viewLocals);

                        delete enteringData.direction;
                        delete enteringData.transition;
                        viewScope.$emit('$ionicView.loaded', enteringData);
                    }

                    // update that this view was just accessed
                    enteringEle.data(DATA_VIEW_ACCESSED, Date.now());

                    callback && callback();
                },

                transition: function (direction, enableBack, allowAnimate) {
                    var deferred;
                    var enteringData = getTransitionData(viewLocals, enteringEle, direction, enteringView);
                    var leavingData = extend(extend({}, enteringData), getViewData(leavingView));
                    enteringData.transitionId = leavingData.transitionId = transitionId;
                    enteringData.fromCache = !!alreadyInDom;
                    enteringData.enableBack = !!enableBack;
                    enteringData.renderStart = renderStart;
                    enteringData.renderEnd = renderEnd;

                    cachedAttr(enteringEle.parent(), 'nav-view-transition', enteringData.transition);
                    cachedAttr(enteringEle.parent(), 'nav-view-direction', enteringData.direction);

                    // cancel any previous transition complete fallbacks
                    $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER));

                    // get the transition ready and see if it'll animate
                    var transitionFn = $ionicConfig.transitions.views[enteringData.transition] || $ionicConfig.transitions.views.none;
                    var viewTransition = transitionFn(enteringEle, leavingEle, enteringData.direction,
                                            enteringData.shouldAnimate && allowAnimate && renderEnd);

                    if (viewTransition.shouldAnimate) {
                        // attach transitionend events (and fallback timer)
                        enteringEle.on(TRANSITIONEND_EVENT, completeOnTransitionEnd);
                        enteringEle.data(DATA_FALLBACK_TIMER, $timeout(transitionComplete, defaultTimeout));
                        $ionicClickBlock.show(defaultTimeout);
                    }

                    if (renderStart) {
                        // notify the views "before" the transition starts
                        switcher.emit('before', enteringData, leavingData);

                        // stage entering element, opacity 0, no transition duration
                        navViewAttr(enteringEle, VIEW_STATUS_STAGED);

                        // render the elements in the correct location for their starting point
                        viewTransition.run(0);
                    }

                    if (renderEnd) {
                        // create a promise so we can keep track of when all transitions finish
                        // only required if this transition should complete
                        deferred = $q.defer();
                        transitionPromises.push(deferred.promise);
                    }

                    if (renderStart && renderEnd) {
                        // CSS "auto" transitioned, not manually transitioned
                        // wait a frame so the styles apply before auto transitioning
                        $timeout(function () {
                            ionic.requestAnimationFrame(onReflow);
                        });
                    } else if (!renderEnd) {
                        // just the start of a manual transition
                        // but it will not render the end of the transition
                        navViewAttr(enteringEle, 'entering');
                        navViewAttr(leavingEle, 'leaving');

                        // return the transition run method so each step can be ran manually
                        return {
                            run: viewTransition.run,
                            cancel: function (shouldAnimate) {
                                if (shouldAnimate) {
                                    enteringEle.on(TRANSITIONEND_EVENT, cancelOnTransitionEnd);
                                    enteringEle.data(DATA_FALLBACK_TIMER, $timeout(cancelTransition, defaultTimeout));
                                    $ionicClickBlock.show(defaultTimeout);
                                } else {
                                    cancelTransition();
                                }
                                viewTransition.shouldAnimate = shouldAnimate;
                                viewTransition.run(0);
                                viewTransition = null;
                            }
                        };

                    } else if (renderEnd) {
                        // just the end of a manual transition
                        // happens after the manual transition has completed
                        // and a full history change has happened
                        onReflow();
                    }


                    function onReflow() {
                        // remove that we're staging the entering element so it can auto transition
                        navViewAttr(enteringEle, viewTransition.shouldAnimate ? 'entering' : VIEW_STATUS_ACTIVE);
                        navViewAttr(leavingEle, viewTransition.shouldAnimate ? 'leaving' : VIEW_STATUS_CACHED);

                        // start the auto transition and let the CSS take over
                        viewTransition.run(1);

                        // trigger auto transitions on the associated nav bars
                        $ionicNavBarDelegate._instances.forEach(function (instance) {
                            instance.triggerTransitionStart(transitionId);
                        });

                        if (!viewTransition.shouldAnimate) {
                            // no animated auto transition
                            transitionComplete();
                        }
                    }

                    // Make sure that transitionend events bubbling up from children won't fire
                    // transitionComplete. Will only go forward if ev.target == the element listening.
                    function completeOnTransitionEnd(ev) {
                        if (ev.target !== this) return;
                        transitionComplete();
                    }
                    function transitionComplete() {
                        if (transitionComplete.x) return;
                        transitionComplete.x = true;

                        enteringEle.off(TRANSITIONEND_EVENT, completeOnTransitionEnd);
                        $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER));
                        leavingEle && $timeout.cancel(leavingEle.data(DATA_FALLBACK_TIMER));

                        // resolve that this one transition (there could be many w/ nested views)
                        deferred && deferred.resolve(navViewCtrl);

                        // the most recent transition added has completed and all the active
                        // transition promises should be added to the services array of promises
                        if (transitionId === transitionCounter) {
                            $q.all(transitionPromises).then(ionicViewSwitcher.transitionEnd);

                            // emit that the views have finished transitioning
                            // each parent nav-view will update which views are active and cached
                            switcher.emit('after', enteringData, leavingData);
                            switcher.cleanup(enteringData);
                        }

                        // tell the nav bars that the transition has ended
                        $ionicNavBarDelegate._instances.forEach(function (instance) {
                            instance.triggerTransitionEnd();
                        });


                        // remove any references that could cause memory issues
                        nextTransition = nextDirection = enteringView = leavingView = enteringEle = leavingEle = null;
                    }

                    // Make sure that transitionend events bubbling up from children won't fire
                    // transitionComplete. Will only go forward if ev.target == the element listening.
                    function cancelOnTransitionEnd(ev) {
                        if (ev.target !== this) return;
                        cancelTransition();
                    }
                    function cancelTransition() {
                        navViewAttr(enteringEle, VIEW_STATUS_CACHED);
                        navViewAttr(leavingEle, VIEW_STATUS_ACTIVE);
                        enteringEle.off(TRANSITIONEND_EVENT, cancelOnTransitionEnd);
                        $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER));
                        ionicViewSwitcher.transitionEnd([navViewCtrl]);
                    }

                },

                emit: function (step, enteringData, leavingData) {
                    var enteringScope = getScopeForElement(enteringEle, enteringData);
                    var leavingScope = getScopeForElement(leavingEle, leavingData);

                    var prefixesAreEqual;

                    if (!enteringData.viewId || enteringData.abstractView) {
                        // it's an abstract view, so treat it accordingly

                        // we only get access to the leaving scope once in the transition,
                        // so dispatch all events right away if it exists
                        if (leavingScope) {
                            leavingScope.$emit('$ionicView.beforeLeave', leavingData);
                            leavingScope.$emit('$ionicView.leave', leavingData);
                            leavingScope.$emit('$ionicView.afterLeave', leavingData);
                            leavingScope.$broadcast('$ionicParentView.beforeLeave', leavingData);
                            leavingScope.$broadcast('$ionicParentView.leave', leavingData);
                            leavingScope.$broadcast('$ionicParentView.afterLeave', leavingData);
                        }
                    }
                    else {
                        // it's a regular view, so do the normal process
                        if (step == 'after') {
                            if (enteringScope) {
                                enteringScope.$emit('$ionicView.enter', enteringData);
                                enteringScope.$broadcast('$ionicParentView.enter', enteringData);
                            }

                            if (leavingScope) {
                                leavingScope.$emit('$ionicView.leave', leavingData);
                                leavingScope.$broadcast('$ionicParentView.leave', leavingData);
                            }
                            else if (enteringScope && leavingData && leavingData.viewId && enteringData.stateName !== leavingData.stateName) {
                                // we only want to dispatch this when we are doing a single-tier
                                // state change such as changing a tab, so compare the state
                                // for the same state-prefix but different suffix
                                prefixesAreEqual = compareStatePrefixes(enteringData.stateName, leavingData.stateName);
                                if (prefixesAreEqual) {
                                    enteringScope.$emit('$ionicNavView.leave', leavingData);
                                }
                            }
                        }

                        if (enteringScope) {
                            enteringScope.$emit('$ionicView.' + step + 'Enter', enteringData);
                            enteringScope.$broadcast('$ionicParentView.' + step + 'Enter', enteringData);
                        }

                        if (leavingScope) {
                            leavingScope.$emit('$ionicView.' + step + 'Leave', leavingData);
                            leavingScope.$broadcast('$ionicParentView.' + step + 'Leave', leavingData);

                        } else if (enteringScope && leavingData && leavingData.viewId && enteringData.stateName !== leavingData.stateName) {
                            // we only want to dispatch this when we are doing a single-tier
                            // state change such as changing a tab, so compare the state
                            // for the same state-prefix but different suffix
                            prefixesAreEqual = compareStatePrefixes(enteringData.stateName, leavingData.stateName);
                            if (prefixesAreEqual) {
                                enteringScope.$emit('$ionicNavView.' + step + 'Leave', leavingData);
                            }
                        }
                    }
                },

                cleanup: function (transData) {
                    // check if any views should be removed
                    if (leavingEle && transData.direction == 'back' && !$ionicConfig.views.forwardCache()) {
                        // if they just navigated back we can destroy the forward view
                        // do not remove forward views if cacheForwardViews config is true
                        destroyViewEle(leavingEle);
                    }

                    var viewElements = navViewCtrl.getViewElements();
                    var viewElementsLength = viewElements.length;
                    var x, viewElement;
                    var removeOldestAccess = (viewElementsLength - 1) > $ionicConfig.views.maxCache();
                    var removableEle;
                    var oldestAccess = Date.now();

                    for (x = 0; x < viewElementsLength; x++) {
                        viewElement = viewElements.eq(x);

                        if (removeOldestAccess && viewElement.data(DATA_VIEW_ACCESSED) < oldestAccess) {
                            // remember what was the oldest element to be accessed so it can be destroyed
                            oldestAccess = viewElement.data(DATA_VIEW_ACCESSED);
                            removableEle = viewElements.eq(x);

                        } else if (viewElement.data(DATA_DESTROY_ELE) && navViewAttr(viewElement) != VIEW_STATUS_ACTIVE) {
                            destroyViewEle(viewElement);
                        }
                    }

                    destroyViewEle(removableEle);

                    if (enteringEle.data(DATA_NO_CACHE)) {
                        enteringEle.data(DATA_DESTROY_ELE, true);
                    }
                },

                enteringEle: function () { return enteringEle; },
                leavingEle: function () { return leavingEle; }

            };

            return switcher;
        },

        transitionEnd: function (navViewCtrls) {
            forEach(navViewCtrls, function (navViewCtrl) {
                navViewCtrl.transitionEnd();
            });

            ionicViewSwitcher.isTransitioning(false);
            $ionicClickBlock.hide();
            transitionPromises = [];
        },

        nextTransition: function (val) {
            nextTransition = val;
        },

        nextDirection: function (val) {
            nextDirection = val;
        },

        isTransitioning: function (val) {
            if (arguments.length) {
                ionic.transition.isActive = !!val;
                $timeout.cancel(isActiveTimer);
                if (val) {
                    isActiveTimer = $timeout(function () {
                        ionicViewSwitcher.isTransitioning(false);
                    }, 999);
                }
            }
            return ionic.transition.isActive;
        },

        createViewEle: function (viewLocals) {
            var containerEle = $document[0].createElement('div');
            if (viewLocals && viewLocals.$template) {
                containerEle.innerHTML = viewLocals.$template;
                if (containerEle.children.length === 1) {
                    containerEle.children[0].classList.add('pane');
                    if (viewLocals.$$state && viewLocals.$$state.self && viewLocals.$$state.self['abstract']) {
                        angular.element(containerEle.children[0]).attr("abstract", "true");
                    }
                    else {
                        if (viewLocals.$$state && viewLocals.$$state.self) {
                            angular.element(containerEle.children[0]).attr("state", viewLocals.$$state.self.name);
                        }

                    }
                    return jqLite(containerEle.children[0]);
                }
            }
            containerEle.className = "pane";
            return jqLite(containerEle);
        },

        viewEleIsActive: function (viewEle, isActiveAttr) {
            navViewAttr(viewEle, isActiveAttr ? VIEW_STATUS_ACTIVE : VIEW_STATUS_CACHED);
        },

        getTransitionData: getTransitionData,
        navViewAttr: navViewAttr,
        destroyViewEle: destroyViewEle

    };

    return ionicViewSwitcher;


    function getViewElementIdentifier(locals, view) {
        if (viewState(locals)['abstract']) return viewState(locals).name;
        if (view) return view.stateId || view.viewId;
        return ionic.Utils.nextUid();
    }

    function viewState(locals) {
        return locals && locals.$$state && locals.$$state.self || {};
    }

    function getTransitionData(viewLocals, enteringEle, direction, view) {
        // Priority
        // 1) attribute directive on the button/link to this view
        // 2) entering element's attribute
        // 3) entering view's $state config property
        // 4) view registration data
        // 5) global config
        // 6) fallback value

        var state = viewState(viewLocals);
        var viewTransition = nextTransition || cachedAttr(enteringEle, 'view-transition') || state.viewTransition || $ionicConfig.views.transition() || 'ios';
        var navBarTransition = $ionicConfig.navBar.transition();
        direction = nextDirection || cachedAttr(enteringEle, 'view-direction') || state.viewDirection || direction || 'none';

        return extend(getViewData(view), {
            transition: viewTransition,
            navBarTransition: navBarTransition === 'view' ? viewTransition : navBarTransition,
            direction: direction,
            shouldAnimate: (viewTransition !== 'none' && direction !== 'none')
        });
    }

    function getViewData(view) {
        view = view || {};
        return {
            viewId: view.viewId,
            historyId: view.historyId,
            stateId: view.stateId,
            stateName: view.stateName,
            stateParams: view.stateParams
        };
    }

    function navViewAttr(ele, value) {
        if (arguments.length > 1) {
            cachedAttr(ele, NAV_VIEW_ATTR, value);
        } else {
            return cachedAttr(ele, NAV_VIEW_ATTR);
        }
    }

    function destroyViewEle(ele) {
        // we found an element that should be removed
        // destroy its scope, then remove the element
        if (ele && ele.length) {
            var viewScope = ele.scope();
            if (viewScope) {
                viewScope.$emit('$ionicView.unloaded', ele.data(DATA_VIEW));
                viewScope.$destroy();
            }
            ele.remove();
        }
    }

    function compareStatePrefixes(enteringStateName, exitingStateName) {
        var enteringStateSuffixIndex = enteringStateName.lastIndexOf('.');
        var exitingStateSuffixIndex = exitingStateName.lastIndexOf('.');

        // if either of the prefixes are empty, just return false
        if (enteringStateSuffixIndex < 0 || exitingStateSuffixIndex < 0) {
            return false;
        }

        var enteringPrefix = enteringStateName.substring(0, enteringStateSuffixIndex);
        var exitingPrefix = exitingStateName.substring(0, exitingStateSuffixIndex);

        return enteringPrefix === exitingPrefix;
    }

    function getScopeForElement(element, stateData) {
        if (!element) {
            return null;
        }
        // check if it's abstract
        var attributeValue = angular.element(element).attr("abstract");
        var stateValue = angular.element(element).attr("state");

        if (attributeValue !== "true") {
            // it's not an abstract view, so make sure the element
            // matches the state.  Due to abstract view weirdness,
            // sometimes it doesn't. If it doesn't, don't dispatch events
            // so leave the scope undefined
            if (stateValue === stateData.stateName) {
                return angular.element(element).scope();
            }
            return null;
        }
        else {
            // it is an abstract element, so look for element with the "state" attributeValue
            // set to the name of the stateData state
            var elements = aggregateNavViewChildren(element);
            for (var i = 0; i < elements.length; i++) {
                var state = angular.element(elements[i]).attr("state");
                if (state === stateData.stateName) {
                    stateData.abstractView = true;
                    return angular.element(elements[i]).scope();
                }
            }
            // we didn't find a match, so return null
            return null;
        }
    }

    function aggregateNavViewChildren(element) {
        var aggregate = [];
        var navViews = angular.element(element).find("ion-nav-view");
        for (var i = 0; i < navViews.length; i++) {
            var children = angular.element(navViews[i]).children();
            var childrenAggregated = [];
            for (var j = 0; j < children.length; j++) {
                childrenAggregated = childrenAggregated.concat(children[j]);
            }
            aggregate = aggregate.concat(childrenAggregated);
        }
        return aggregate;
    }

} ]);

    /**
    * ==================  angular-ios9-uiwebview.patch.js v1.1.1 ==================
    *
    * This patch works around iOS9 UIWebView regression that causes infinite digest
    * errors in Angular.
    *
    * The patch can be applied to Angular 1.2.0 – 1.4.5. Newer versions of Angular
    * have the workaround baked in.
    *
    * To apply this patch load/bundle this file with your application and add a
    * dependency on the "ngIOS9UIWebViewPatch" module to your main app module.
    *
    * For example:
    *
    * ```
    * angular.module('myApp', ['ngRoute'])`
    * ```
    *
    * becomes
    *
    * ```
    * angular.module('myApp', ['ngRoute', 'ngIOS9UIWebViewPatch'])
    * ```
    *
    *
    * More info:
    * - https://openradar.appspot.com/22186109
    * - https://github.com/angular/angular.js/issues/12241
    * - https://github.com/driftyco/ionic/issues/4082
    *
    *
    * @license AngularJS
    * (c) 2010-2015 Google, Inc. http://angularjs.org
    * License: MIT
    */

    angular.module('ngIOS9UIWebViewPatch', ['ng']).config(['$provide', function ($provide) {
        'use strict';

        $provide.decorator('$browser', ['$delegate', '$window', function ($delegate, $window) {

            if (isIOS9UIWebView($window.navigator.userAgent)) {
                return applyIOS9Shim($delegate);
            }

            return $delegate;

            function isIOS9UIWebView(userAgent) {
                return /(iPhone|iPad|iPod).* OS 9_\d/.test(userAgent) && !/Version\/9\./.test(userAgent);
            }

            function applyIOS9Shim(browser) {
                var pendingLocationUrl = null;
                var originalUrlFn = browser.url;

                browser.url = function () {
                    if (arguments.length) {
                        pendingLocationUrl = arguments[0];
                        return originalUrlFn.apply(browser, arguments);
                    }

                    return pendingLocationUrl || originalUrlFn.apply(browser, arguments);
                };

                window.addEventListener('popstate', clearPendingLocationUrl, false);
                window.addEventListener('hashchange', clearPendingLocationUrl, false);

                function clearPendingLocationUrl() {
                    pendingLocationUrl = null;
                }

                return browser;
            }
        } ]);
    } ]);

    /**
    * @private
    * Parts of Ionic requires that $scope data is attached to the element.
    * We do not want to disable adding $scope data to the $element when
    * $compileProvider.debugInfoEnabled(false) is used.
    */
    IonicModule.config(['$provide', function ($provide) {
        $provide.decorator('$compile', ['$delegate', function ($compile) {
            $compile.$$addScopeInfo = function $$addScopeInfo($element, scope, isolated, noTemplate) {
                var dataName = isolated ? (noTemplate ? '$isolateScopeNoTemplate' : '$isolateScope') : '$scope';
                $element.data(dataName, scope);
            };
            return $compile;
        } ]);
    } ]);

    /**
    * @private
    */
    IonicModule.config([
  '$provide',
function ($provide) {
    function $LocationDecorator($location, $timeout) {

        $location.__hash = $location.hash;
        //Fix: when window.location.hash is set, the scrollable area
        //found nearest to body's scrollTop is set to scroll to an element
        //with that ID.
        $location.hash = function (value) {
            if (isDefined(value) && value.length > 0) {
                $timeout(function () {
                    var scroll = document.querySelector('.scroll-content');
                    if (scroll) {
                        scroll.scrollTop = 0;
                    }
                }, 0, false);
            }
            return $location.__hash(value);
        };

        return $location;
    }

    $provide.decorator('$location', ['$delegate', '$timeout', $LocationDecorator]);
} ]);

    IonicModule

.controller('$ionicHeaderBar', [
  '$scope',
  '$element',
  '$attrs',
  '$q',
  '$ionicConfig',
  '$ionicHistory',
function ($scope, $element, $attrs, $q, $ionicConfig, $ionicHistory) {
    var TITLE = 'title';
    var BACK_TEXT = 'back-text';
    var BACK_BUTTON = 'back-button';
    var DEFAULT_TITLE = 'default-title';
    var PREVIOUS_TITLE = 'previous-title';
    var HIDE = 'hide';

    var self = this;
    var titleText = '';
    var previousTitleText = '';
    var titleLeft = 0;
    var titleRight = 0;
    var titleCss = '';
    var isBackEnabled = false;
    var isBackShown = true;
    var isNavBackShown = true;
    var isBackElementShown = false;
    var titleTextWidth = 0;


    self.beforeEnter = function (viewData) {
        $scope.$broadcast('$ionicView.beforeEnter', viewData);
    };


    self.title = function (newTitleText) {
        if (arguments.length && newTitleText !== titleText) {
            getEle(TITLE).innerHTML = newTitleText;
            titleText = newTitleText;
            titleTextWidth = 0;
        }
        return titleText;
    };


    self.enableBack = function (shouldEnable, disableReset) {
        // whether or not the back button show be visible, according
        // to the navigation and history
        if (arguments.length) {
            isBackEnabled = shouldEnable;
            if (!disableReset) self.updateBackButton();
        }
        return isBackEnabled;
    };


    self.showBack = function (shouldShow, disableReset) {
        // different from enableBack() because this will always have the back
        // visually hidden if false, even if the history says it should show
        if (arguments.length) {
            isBackShown = shouldShow;
            if (!disableReset) self.updateBackButton();
        }
        return isBackShown;
    };


    self.showNavBack = function (shouldShow) {
        // different from showBack() because this is for the entire nav bar's
        // setting for all of it's child headers. For internal use.
        isNavBackShown = shouldShow;
        self.updateBackButton();
    };


    self.updateBackButton = function () {
        var ele;
        if ((isBackShown && isNavBackShown && isBackEnabled) !== isBackElementShown) {
            isBackElementShown = isBackShown && isNavBackShown && isBackEnabled;
            ele = getEle(BACK_BUTTON);
            ele && ele.classList[isBackElementShown ? 'remove' : 'add'](HIDE);
        }

        if (isBackEnabled) {
            ele = ele || getEle(BACK_BUTTON);
            if (ele) {
                if (self.backButtonIcon !== $ionicConfig.backButton.icon()) {
                    ele = getEle(BACK_BUTTON + ' .icon');
                    if (ele) {
                        self.backButtonIcon = $ionicConfig.backButton.icon();
                        ele.className = 'icon ' + self.backButtonIcon;
                    }
                }

                if (self.backButtonText !== $ionicConfig.backButton.text()) {
                    ele = getEle(BACK_BUTTON + ' .back-text');
                    if (ele) {
                        ele.textContent = self.backButtonText = $ionicConfig.backButton.text();
                    }
                }
            }
        }
    };


    self.titleTextWidth = function () {
        var element = getEle(TITLE);
        if (element) {
            // If the element has a nav-bar-title, use that instead
            // to calculate the width of the title
            var children = angular.element(element).children();
            for (var i = 0; i < children.length; i++) {
                if (angular.element(children[i]).hasClass('nav-bar-title')) {
                    element = children[i];
                    break;
                }
            }
        }
        var bounds = ionic.DomUtil.getTextBounds(element);
        titleTextWidth = Math.min(bounds && bounds.width || 30);
        return titleTextWidth;
    };


    self.titleWidth = function () {
        var titleWidth = self.titleTextWidth();
        var offsetWidth = getEle(TITLE).offsetWidth;
        if (offsetWidth < titleWidth) {
            titleWidth = offsetWidth + (titleLeft - titleRight - 5);
        }
        return titleWidth;
    };


    self.titleTextX = function () {
        return ($element[0].offsetWidth / 2) - (self.titleWidth() / 2);
    };


    self.titleLeftRight = function () {
        return titleLeft - titleRight;
    };


    self.backButtonTextLeft = function () {
        var offsetLeft = 0;
        var ele = getEle(BACK_TEXT);
        while (ele) {
            offsetLeft += ele.offsetLeft;
            ele = ele.parentElement;
        }
        return offsetLeft;
    };


    self.resetBackButton = function (viewData) {
        if ($ionicConfig.backButton.previousTitleText()) {
            var previousTitleEle = getEle(PREVIOUS_TITLE);
            if (previousTitleEle) {
                previousTitleEle.classList.remove(HIDE);

                var view = (viewData && $ionicHistory.getViewById(viewData.viewId));
                var newPreviousTitleText = $ionicHistory.backTitle(view);

                if (newPreviousTitleText !== previousTitleText) {
                    previousTitleText = previousTitleEle.innerHTML = newPreviousTitleText;
                }
            }
            var defaultTitleEle = getEle(DEFAULT_TITLE);
            if (defaultTitleEle) {
                defaultTitleEle.classList.remove(HIDE);
            }
        }
    };


    self.align = function (textAlign) {
        var titleEle = getEle(TITLE);

        textAlign = textAlign || $attrs.alignTitle || $ionicConfig.navBar.alignTitle();

        var widths = self.calcWidths(textAlign, false);

        if (isBackShown && previousTitleText && $ionicConfig.backButton.previousTitleText()) {
            var previousTitleWidths = self.calcWidths(textAlign, true);

            var availableTitleWidth = $element[0].offsetWidth - previousTitleWidths.titleLeft - previousTitleWidths.titleRight;

            if (self.titleTextWidth() <= availableTitleWidth) {
                widths = previousTitleWidths;
            }
        }

        return self.updatePositions(titleEle, widths.titleLeft, widths.titleRight, widths.buttonsLeft, widths.buttonsRight, widths.css, widths.showPrevTitle);
    };


    self.calcWidths = function (textAlign, isPreviousTitle) {
        var titleEle = getEle(TITLE);
        var backBtnEle = getEle(BACK_BUTTON);
        var x, y, z, b, c, d, childSize, bounds;
        var childNodes = $element[0].childNodes;
        var buttonsLeft = 0;
        var buttonsRight = 0;
        var isCountRightOfTitle;
        var updateTitleLeft = 0;
        var updateTitleRight = 0;
        var updateCss = '';
        var backButtonWidth = 0;

        // Compute how wide the left children are
        // Skip all titles (there may still be two titles, one leaving the dom)
        // Once we encounter a titleEle, realize we are now counting the right-buttons, not left
        for (x = 0; x < childNodes.length; x++) {
            c = childNodes[x];

            childSize = 0;
            if (c.nodeType == 1) {
                // element node
                if (c === titleEle) {
                    isCountRightOfTitle = true;
                    continue;
                }

                if (c.classList.contains(HIDE)) {
                    continue;
                }

                if (isBackShown && c === backBtnEle) {

                    for (y = 0; y < c.childNodes.length; y++) {
                        b = c.childNodes[y];

                        if (b.nodeType == 1) {

                            if (b.classList.contains(BACK_TEXT)) {
                                for (z = 0; z < b.children.length; z++) {
                                    d = b.children[z];

                                    if (isPreviousTitle) {
                                        if (d.classList.contains(DEFAULT_TITLE)) continue;
                                        backButtonWidth += d.offsetWidth;
                                    } else {
                                        if (d.classList.contains(PREVIOUS_TITLE)) continue;
                                        backButtonWidth += d.offsetWidth;
                                    }
                                }

                            } else {
                                backButtonWidth += b.offsetWidth;
                            }

                        } else if (b.nodeType == 3 && b.nodeValue.trim()) {
                            bounds = ionic.DomUtil.getTextBounds(b);
                            backButtonWidth += bounds && bounds.width || 0;
                        }

                    }
                    childSize = backButtonWidth || c.offsetWidth;

                } else {
                    // not the title, not the back button, not a hidden element
                    childSize = c.offsetWidth;
                }

            } else if (c.nodeType == 3 && c.nodeValue.trim()) {
                // text node
                bounds = ionic.DomUtil.getTextBounds(c);
                childSize = bounds && bounds.width || 0;
            }

            if (isCountRightOfTitle) {
                buttonsRight += childSize;
            } else {
                buttonsLeft += childSize;
            }
        }

        // Size and align the header titleEle based on the sizes of the left and
        // right children, and the desired alignment mode
        if (textAlign == 'left') {
            updateCss = 'title-left';
            if (buttonsLeft) {
                updateTitleLeft = buttonsLeft + 15;
            }
            if (buttonsRight) {
                updateTitleRight = buttonsRight + 15;
            }

        } else if (textAlign == 'right') {
            updateCss = 'title-right';
            if (buttonsLeft) {
                updateTitleLeft = buttonsLeft + 15;
            }
            if (buttonsRight) {
                updateTitleRight = buttonsRight + 15;
            }

        } else {
            // center the default
            var margin = Math.max(buttonsLeft, buttonsRight) + 10;
            if (margin > 10) {
                updateTitleLeft = updateTitleRight = margin;
            }
        }

        return {
            backButtonWidth: backButtonWidth,
            buttonsLeft: buttonsLeft,
            buttonsRight: buttonsRight,
            titleLeft: updateTitleLeft,
            titleRight: updateTitleRight,
            showPrevTitle: isPreviousTitle,
            css: updateCss
        };
    };


    self.updatePositions = function (titleEle, updateTitleLeft, updateTitleRight, buttonsLeft, buttonsRight, updateCss, showPreviousTitle) {
        var deferred = $q.defer();

        // only make DOM updates when there are actual changes
        if (titleEle) {
            if (updateTitleLeft !== titleLeft) {
                titleEle.style.left = updateTitleLeft ? updateTitleLeft + 'px' : '';
                titleLeft = updateTitleLeft;
            }
            if (updateTitleRight !== titleRight) {
                titleEle.style.right = updateTitleRight ? updateTitleRight + 'px' : '';
                titleRight = updateTitleRight;
            }

            if (updateCss !== titleCss) {
                updateCss && titleEle.classList.add(updateCss);
                titleCss && titleEle.classList.remove(titleCss);
                titleCss = updateCss;
            }
        }

        if ($ionicConfig.backButton.previousTitleText()) {
            var prevTitle = getEle(PREVIOUS_TITLE);
            var defaultTitle = getEle(DEFAULT_TITLE);

            prevTitle && prevTitle.classList[showPreviousTitle ? 'remove' : 'add'](HIDE);
            defaultTitle && defaultTitle.classList[showPreviousTitle ? 'add' : 'remove'](HIDE);
        }

        ionic.requestAnimationFrame(function () {
            if (titleEle && titleEle.offsetWidth + 10 < titleEle.scrollWidth) {
                var minRight = buttonsRight + 5;
                var testRight = $element[0].offsetWidth - titleLeft - self.titleTextWidth() - 20;
                updateTitleRight = testRight < minRight ? minRight : testRight;
                if (updateTitleRight !== titleRight) {
                    titleEle.style.right = updateTitleRight + 'px';
                    titleRight = updateTitleRight;
                }
            }
            deferred.resolve();
        });

        return deferred.promise;
    };


    self.setCss = function (elementClassname, css) {
        ionic.DomUtil.cachedStyles(getEle(elementClassname), css);
    };


    var eleCache = {};
    function getEle(className) {
        if (!eleCache[className]) {
            eleCache[className] = $element[0].querySelector('.' + className);
        }
        return eleCache[className];
    }


    $scope.$on('$destroy', function () {
        for (var n in eleCache) eleCache[n] = null;
    });

} ]);

    IonicModule
.controller('$ionInfiniteScroll', [
  '$scope',
  '$attrs',
  '$element',
  '$timeout',
function ($scope, $attrs, $element, $timeout) {
    var self = this;
    self.isLoading = false;

    $scope.icon = function () {
        return isDefined($attrs.icon) ? $attrs.icon : 'ion-load-d';
    };

    $scope.spinner = function () {
        return isDefined($attrs.spinner) ? $attrs.spinner : '';
    };

    $scope.$on('scroll.infiniteScrollComplete', function () {
        finishInfiniteScroll();
    });

    $scope.$on('$destroy', function () {
        if (self.scrollCtrl && self.scrollCtrl.$element) self.scrollCtrl.$element.off('scroll', self.checkBounds);
        if (self.scrollEl && self.scrollEl.removeEventListener) {
            self.scrollEl.removeEventListener('scroll', self.checkBounds);
        }
    });

    // debounce checking infinite scroll events
    self.checkBounds = ionic.Utils.throttle(checkInfiniteBounds, 300);

    function onInfinite() {
        ionic.requestAnimationFrame(function () {
            $element[0].classList.add('active');
        });
        self.isLoading = true;
        $scope.$parent && $scope.$parent.$apply($attrs.onInfinite || '');
    }

    function finishInfiniteScroll() {
        ionic.requestAnimationFrame(function () {
            $element[0].classList.remove('active');
        });
        $timeout(function () {
            if (self.jsScrolling) self.scrollView.resize();
            // only check bounds again immediately if the page isn't cached (scroll el has height)
            if ((self.jsScrolling && self.scrollView.__container && self.scrollView.__container.offsetHeight > 0) ||
      !self.jsScrolling) {
                self.checkBounds();
            }
        }, 30, false);
        self.isLoading = false;
    }

    // check if we've scrolled far enough to trigger an infinite scroll
    function checkInfiniteBounds() {
        if (self.isLoading) return;
        var maxScroll = {};

        if (self.jsScrolling) {
            maxScroll = self.getJSMaxScroll();
            var scrollValues = self.scrollView.getValues();
            if ((maxScroll.left !== -1 && scrollValues.left >= maxScroll.left) ||
        (maxScroll.top !== -1 && scrollValues.top >= maxScroll.top)) {
                onInfinite();
            }
        } else {
            maxScroll = self.getNativeMaxScroll();
            if ((
        maxScroll.left !== -1 &&
        self.scrollEl.scrollLeft >= maxScroll.left - self.scrollEl.clientWidth
        ) || (
        maxScroll.top !== -1 &&
        self.scrollEl.scrollTop >= maxScroll.top - self.scrollEl.clientHeight
        )) {
                onInfinite();
            }
        }
    }

    // determine the threshold at which we should fire an infinite scroll
    // note: this gets processed every scroll event, can it be cached?
    self.getJSMaxScroll = function () {
        var maxValues = self.scrollView.getScrollMax();
        return {
            left: self.scrollView.options.scrollingX ?
        calculateMaxValue(maxValues.left) :
        -1,
            top: self.scrollView.options.scrollingY ?
        calculateMaxValue(maxValues.top) :
        -1
        };
    };

    self.getNativeMaxScroll = function () {
        var maxValues = {
            left: self.scrollEl.scrollWidth,
            top: self.scrollEl.scrollHeight
        };
        var computedStyle = window.getComputedStyle(self.scrollEl) || {};
        return {
            left: maxValues.left &&
        (computedStyle.overflowX === 'scroll' ||
        computedStyle.overflowX === 'auto' ||
        self.scrollEl.style['overflow-x'] === 'scroll') ?
        calculateMaxValue(maxValues.left) : -1,
            top: maxValues.top &&
        (computedStyle.overflowY === 'scroll' ||
        computedStyle.overflowY === 'auto' ||
        self.scrollEl.style['overflow-y'] === 'scroll') ?
        calculateMaxValue(maxValues.top) : -1
        };
    };

    // determine pixel refresh distance based on % or value
    function calculateMaxValue(maximum) {
        var distance = ($attrs.distance || '2.5%').trim();
        var isPercent = distance.indexOf('%') !== -1;
        return isPercent ?
    maximum * (1 - parseFloat(distance) / 100) :
    maximum - parseFloat(distance);
    }

    //for testing
    self.__finishInfiniteScroll = finishInfiniteScroll;

} ]);

    /**
    * @ngdoc service
    * @name $ionicListDelegate
    * @module ionic
    *
    * @description
    * Delegate for controlling the {@link ionic.directive:ionList} directive.
    *
    * Methods called directly on the $ionicListDelegate service will control all lists.
    * Use the {@link ionic.service:$ionicListDelegate#$getByHandle $getByHandle}
    * method to control specific ionList instances.
    *
    * @usage
    * ```html
    * {% raw %}
    * <ion-content ng-controller="MyCtrl">
    *   <button class="button" ng-click="showDeleteButtons()"></button>
    *   <ion-list>
    *     <ion-item ng-repeat="i in items">
    *       Hello, {{i}}!
    *       <ion-delete-button class="ion-minus-circled"></ion-delete-button>
    *     </ion-item>
    *   </ion-list>
    * </ion-content>
    * {% endraw %}
    * ```

    * ```js
    * function MyCtrl($scope, $ionicListDelegate) {
    *   $scope.showDeleteButtons = function() {
    *     $ionicListDelegate.showDelete(true);
    *   };
    * }
    * ```
    */
    IonicModule.service('$ionicListDelegate', ionic.DelegateService([
    /**
    * @ngdoc method
    * @name $ionicListDelegate#showReorder
    * @param {boolean=} showReorder Set whether or not this list is showing its reorder buttons.
    * @returns {boolean} Whether the reorder buttons are shown.
    */
  'showReorder',
    /**
    * @ngdoc method
    * @name $ionicListDelegate#showDelete
    * @param {boolean=} showDelete Set whether or not this list is showing its delete buttons.
    * @returns {boolean} Whether the delete buttons are shown.
    */
  'showDelete',
    /**
    * @ngdoc method
    * @name $ionicListDelegate#canSwipeItems
    * @param {boolean=} canSwipeItems Set whether or not this list is able to swipe to show
    * option buttons.
    * @returns {boolean} Whether the list is able to swipe to show option buttons.
    */
  'canSwipeItems',
    /**
    * @ngdoc method
    * @name $ionicListDelegate#closeOptionButtons
    * @description Closes any option buttons on the list that are swiped open.
    */
  'closeOptionButtons'
    /**
    * @ngdoc method
    * @name $ionicListDelegate#$getByHandle
    * @param {string} handle
    * @returns `delegateInstance` A delegate instance that controls only the
    * {@link ionic.directive:ionList} directives with `delegate-handle` matching
    * the given handle.
    *
    * Example: `$ionicListDelegate.$getByHandle('my-handle').showReorder(true);`
    */
]))

.controller('$ionicList', [
  '$scope',
  '$attrs',
  '$ionicListDelegate',
  '$ionicHistory',
function ($scope, $attrs, $ionicListDelegate, $ionicHistory) {
    var self = this;
    var isSwipeable = true;
    var isReorderShown = false;
    var isDeleteShown = false;

    var deregisterInstance = $ionicListDelegate._registerInstance(
    self, $attrs.delegateHandle, function () {
        return $ionicHistory.isActiveScope($scope);
    }
  );
    $scope.$on('$destroy', deregisterInstance);

    self.showReorder = function (show) {
        if (arguments.length) {
            isReorderShown = !!show;
        }
        return isReorderShown;
    };

    self.showDelete = function (show) {
        if (arguments.length) {
            isDeleteShown = !!show;
        }
        return isDeleteShown;
    };

    self.canSwipeItems = function (can) {
        if (arguments.length) {
            isSwipeable = !!can;
        }
        return isSwipeable;
    };

    self.closeOptionButtons = function () {
        self.listView && self.listView.clearDragEffects();
    };
} ]);

    IonicModule

.controller('$ionicNavBar', [
  '$scope',
  '$element',
  '$attrs',
  '$compile',
  '$timeout',
  '$ionicNavBarDelegate',
  '$ionicConfig',
  '$ionicHistory',
function ($scope, $element, $attrs, $compile, $timeout, $ionicNavBarDelegate, $ionicConfig, $ionicHistory) {

    var CSS_HIDE = 'hide';
    var DATA_NAV_BAR_CTRL = '$ionNavBarController';
    var PRIMARY_BUTTONS = 'primaryButtons';
    var SECONDARY_BUTTONS = 'secondaryButtons';
    var BACK_BUTTON = 'backButton';
    var ITEM_TYPES = 'primaryButtons secondaryButtons leftButtons rightButtons title'.split(' ');

    var self = this;
    var headerBars = [];
    var navElementHtml = {};
    var isVisible = true;
    var queuedTransitionStart, queuedTransitionEnd, latestTransitionId;

    $element.parent().data(DATA_NAV_BAR_CTRL, self);

    var delegateHandle = $attrs.delegateHandle || 'navBar' + ionic.Utils.nextUid();

    var deregisterInstance = $ionicNavBarDelegate._registerInstance(self, delegateHandle);


    self.init = function () {
        $element.addClass('nav-bar-container');
        ionic.DomUtil.cachedAttr($element, 'nav-bar-transition', $ionicConfig.views.transition());

        // create two nav bar blocks which will trade out which one is shown
        self.createHeaderBar(false);
        self.createHeaderBar(true);

        $scope.$emit('ionNavBar.init', delegateHandle);
    };


    self.createHeaderBar = function (isActive) {
        var containerEle = jqLite('<div class="nav-bar-block">');
        ionic.DomUtil.cachedAttr(containerEle, 'nav-bar', isActive ? 'active' : 'cached');

        var alignTitle = $attrs.alignTitle || $ionicConfig.navBar.alignTitle();
        var headerBarEle = jqLite('<ion-header-bar>').addClass($attrs['class']).attr('align-title', alignTitle);
        if (isDefined($attrs.noTapScroll)) headerBarEle.attr('no-tap-scroll', $attrs.noTapScroll);
        var titleEle = jqLite('<div class="title title-' + alignTitle + '">');
        var navEle = {};
        var lastViewItemEle = {};
        var leftButtonsEle, rightButtonsEle;

        navEle[BACK_BUTTON] = createNavElement(BACK_BUTTON);
        navEle[BACK_BUTTON] && headerBarEle.append(navEle[BACK_BUTTON]);

        // append title in the header, this is the rock to where buttons append
        headerBarEle.append(titleEle);

        forEach(ITEM_TYPES, function (itemType) {
            // create default button elements
            navEle[itemType] = createNavElement(itemType);
            // append and position buttons
            positionItem(navEle[itemType], itemType);
        });

        // add header-item to the root children
        for (var x = 0; x < headerBarEle[0].children.length; x++) {
            headerBarEle[0].children[x].classList.add('header-item');
        }

        // compile header and append to the DOM
        containerEle.append(headerBarEle);
        $element.append($compile(containerEle)($scope.$new()));

        var headerBarCtrl = headerBarEle.data('$ionHeaderBarController');
        headerBarCtrl.backButtonIcon = $ionicConfig.backButton.icon();
        headerBarCtrl.backButtonText = $ionicConfig.backButton.text();

        var headerBarInstance = {
            isActive: isActive,
            title: function (newTitleText) {
                headerBarCtrl.title(newTitleText);
            },
            setItem: function (navBarItemEle, itemType) {
                // first make sure any exiting nav bar item has been removed
                headerBarInstance.removeItem(itemType);

                if (navBarItemEle) {
                    if (itemType === 'title') {
                        // clear out the text based title
                        headerBarInstance.title("");
                    }

                    // there's a custom nav bar item
                    positionItem(navBarItemEle, itemType);

                    if (navEle[itemType]) {
                        // make sure the default on this itemType is hidden
                        navEle[itemType].addClass(CSS_HIDE);
                    }
                    lastViewItemEle[itemType] = navBarItemEle;

                } else if (navEle[itemType]) {
                    // there's a default button for this side and no view button
                    navEle[itemType].removeClass(CSS_HIDE);
                }
            },
            removeItem: function (itemType) {
                if (lastViewItemEle[itemType]) {
                    lastViewItemEle[itemType].scope().$destroy();
                    lastViewItemEle[itemType].remove();
                    lastViewItemEle[itemType] = null;
                }
            },
            containerEle: function () {
                return containerEle;
            },
            headerBarEle: function () {
                return headerBarEle;
            },
            afterLeave: function () {
                forEach(ITEM_TYPES, function (itemType) {
                    headerBarInstance.removeItem(itemType);
                });
                headerBarCtrl.resetBackButton();
            },
            controller: function () {
                return headerBarCtrl;
            },
            destroy: function () {
                forEach(ITEM_TYPES, function (itemType) {
                    headerBarInstance.removeItem(itemType);
                });
                containerEle.scope().$destroy();
                for (var n in navEle) {
                    if (navEle[n]) {
                        navEle[n].removeData();
                        navEle[n] = null;
                    }
                }
                leftButtonsEle && leftButtonsEle.removeData();
                rightButtonsEle && rightButtonsEle.removeData();
                titleEle.removeData();
                headerBarEle.removeData();
                containerEle.remove();
                containerEle = headerBarEle = titleEle = leftButtonsEle = rightButtonsEle = null;
            }
        };

        function positionItem(ele, itemType) {
            if (!ele) return;

            if (itemType === 'title') {
                // title element
                titleEle.append(ele);

            } else if (itemType == 'rightButtons' ||
                (itemType == SECONDARY_BUTTONS && $ionicConfig.navBar.positionSecondaryButtons() != 'left') ||
                (itemType == PRIMARY_BUTTONS && $ionicConfig.navBar.positionPrimaryButtons() == 'right')) {
                // right side
                if (!rightButtonsEle) {
                    rightButtonsEle = jqLite('<div class="buttons buttons-right">');
                    headerBarEle.append(rightButtonsEle);
                }
                if (itemType == SECONDARY_BUTTONS) {
                    rightButtonsEle.append(ele);
                } else {
                    rightButtonsEle.prepend(ele);
                }

            } else {
                // left side
                if (!leftButtonsEle) {
                    leftButtonsEle = jqLite('<div class="buttons buttons-left">');
                    if (navEle[BACK_BUTTON]) {
                        navEle[BACK_BUTTON].after(leftButtonsEle);
                    } else {
                        headerBarEle.prepend(leftButtonsEle);
                    }
                }
                if (itemType == SECONDARY_BUTTONS) {
                    leftButtonsEle.append(ele);
                } else {
                    leftButtonsEle.prepend(ele);
                }
            }

        }

        headerBars.push(headerBarInstance);

        return headerBarInstance;
    };


    self.navElement = function (type, html) {
        if (isDefined(html)) {
            navElementHtml[type] = html;
        }
        return navElementHtml[type];
    };


    self.update = function (viewData) {
        var showNavBar = !viewData.hasHeaderBar && viewData.showNavBar;
        viewData.transition = $ionicConfig.views.transition();

        if (!showNavBar) {
            viewData.direction = 'none';
        }

        self.enable(showNavBar);
        var enteringHeaderBar = self.isInitialized ? getOffScreenHeaderBar() : getOnScreenHeaderBar();
        var leavingHeaderBar = self.isInitialized ? getOnScreenHeaderBar() : null;
        var enteringHeaderCtrl = enteringHeaderBar.controller();

        // update if the entering header should show the back button or not
        enteringHeaderCtrl.enableBack(viewData.enableBack, true);
        enteringHeaderCtrl.showBack(viewData.showBack, true);
        enteringHeaderCtrl.updateBackButton();

        // update the entering header bar's title
        self.title(viewData.title, enteringHeaderBar);

        self.showBar(showNavBar);

        // update the nav bar items, depending if the view has their own or not
        if (viewData.navBarItems) {
            forEach(ITEM_TYPES, function (itemType) {
                enteringHeaderBar.setItem(viewData.navBarItems[itemType], itemType);
            });
        }

        // begin transition of entering and leaving header bars
        self.transition(enteringHeaderBar, leavingHeaderBar, viewData);

        self.isInitialized = true;
        navSwipeAttr('');
    };


    self.transition = function (enteringHeaderBar, leavingHeaderBar, viewData) {
        var enteringHeaderBarCtrl = enteringHeaderBar.controller();
        var transitionFn = $ionicConfig.transitions.navBar[viewData.navBarTransition] || $ionicConfig.transitions.navBar.none;
        var transitionId = viewData.transitionId;

        enteringHeaderBarCtrl.beforeEnter(viewData);

        var navBarTransition = transitionFn(enteringHeaderBar, leavingHeaderBar, viewData.direction, viewData.shouldAnimate && self.isInitialized);

        ionic.DomUtil.cachedAttr($element, 'nav-bar-transition', viewData.navBarTransition);
        ionic.DomUtil.cachedAttr($element, 'nav-bar-direction', viewData.direction);

        if (navBarTransition.shouldAnimate && viewData.renderEnd) {
            navBarAttr(enteringHeaderBar, 'stage');
        } else {
            navBarAttr(enteringHeaderBar, 'entering');
            navBarAttr(leavingHeaderBar, 'leaving');
        }

        enteringHeaderBarCtrl.resetBackButton(viewData);

        navBarTransition.run(0);

        self.activeTransition = {
            run: function (step) {
                navBarTransition.shouldAnimate = false;
                navBarTransition.direction = 'back';
                navBarTransition.run(step);
            },
            cancel: function (shouldAnimate, speed, cancelData) {
                navSwipeAttr(speed);
                navBarAttr(leavingHeaderBar, 'active');
                navBarAttr(enteringHeaderBar, 'cached');
                navBarTransition.shouldAnimate = shouldAnimate;
                navBarTransition.run(0);
                self.activeTransition = navBarTransition = null;

                var runApply;
                if (cancelData.showBar !== self.showBar()) {
                    self.showBar(cancelData.showBar);
                }
                if (cancelData.showBackButton !== self.showBackButton()) {
                    self.showBackButton(cancelData.showBackButton);
                }
                if (runApply) {
                    $scope.$apply();
                }
            },
            complete: function (shouldAnimate, speed) {
                navSwipeAttr(speed);
                navBarTransition.shouldAnimate = shouldAnimate;
                navBarTransition.run(1);
                queuedTransitionEnd = transitionEnd;
            }
        };

        $timeout(enteringHeaderBarCtrl.align, 16);

        queuedTransitionStart = function () {
            if (latestTransitionId !== transitionId) return;

            navBarAttr(enteringHeaderBar, 'entering');
            navBarAttr(leavingHeaderBar, 'leaving');

            navBarTransition.run(1);

            queuedTransitionEnd = function () {
                if (latestTransitionId == transitionId || !navBarTransition.shouldAnimate) {
                    transitionEnd();
                }
            };

            queuedTransitionStart = null;
        };

        function transitionEnd() {
            for (var x = 0; x < headerBars.length; x++) {
                headerBars[x].isActive = false;
            }
            enteringHeaderBar.isActive = true;

            navBarAttr(enteringHeaderBar, 'active');
            navBarAttr(leavingHeaderBar, 'cached');

            self.activeTransition = navBarTransition = queuedTransitionEnd = null;
        }

        queuedTransitionStart();
    };


    self.triggerTransitionStart = function (triggerTransitionId) {
        latestTransitionId = triggerTransitionId;
        queuedTransitionStart && queuedTransitionStart();
    };


    self.triggerTransitionEnd = function () {
        queuedTransitionEnd && queuedTransitionEnd();
    };


    self.showBar = function (shouldShow) {
        if (arguments.length) {
            self.visibleBar(shouldShow);
            $scope.$parent.$hasHeader = !!shouldShow;
        }
        return !!$scope.$parent.$hasHeader;
    };


    self.visibleBar = function (shouldShow) {
        if (shouldShow && !isVisible) {
            $element.removeClass(CSS_HIDE);
            self.align();
        } else if (!shouldShow && isVisible) {
            $element.addClass(CSS_HIDE);
        }
        isVisible = shouldShow;
    };


    self.enable = function (val) {
        // set primary to show first
        self.visibleBar(val);

        // set non primary to hide second
        for (var x = 0; x < $ionicNavBarDelegate._instances.length; x++) {
            if ($ionicNavBarDelegate._instances[x] !== self) $ionicNavBarDelegate._instances[x].visibleBar(false);
        }
    };


    /**
    * @ngdoc method
    * @name $ionicNavBar#showBackButton
    * @description Show/hide the nav bar back button when there is a
    * back view. If the back button is not possible, for example, the
    * first view in the stack, then this will not force the back button
    * to show.
    */
    self.showBackButton = function (shouldShow) {
        if (arguments.length) {
            for (var x = 0; x < headerBars.length; x++) {
                headerBars[x].controller().showNavBack(!!shouldShow);
            }
            $scope.$isBackButtonShown = !!shouldShow;
        }
        return $scope.$isBackButtonShown;
    };


    /**
    * @ngdoc method
    * @name $ionicNavBar#showActiveBackButton
    * @description Show/hide only the active header bar's back button.
    */
    self.showActiveBackButton = function (shouldShow) {
        var headerBar = getOnScreenHeaderBar();
        if (headerBar) {
            if (arguments.length) {
                return headerBar.controller().showBack(shouldShow);
            }
            return headerBar.controller().showBack();
        }
    };


    self.title = function (newTitleText, headerBar) {
        if (isDefined(newTitleText)) {
            newTitleText = newTitleText || '';
            headerBar = headerBar || getOnScreenHeaderBar();
            headerBar && headerBar.title(newTitleText);
            $scope.$title = newTitleText;
            $ionicHistory.currentTitle(newTitleText);
        }
        return $scope.$title;
    };


    self.align = function (val, headerBar) {
        headerBar = headerBar || getOnScreenHeaderBar();
        headerBar && headerBar.controller().align(val);
    };


    self.hasTabsTop = function (isTabsTop) {
        $element[isTabsTop ? 'addClass' : 'removeClass']('nav-bar-tabs-top');
    };

    self.hasBarSubheader = function (isBarSubheader) {
        $element[isBarSubheader ? 'addClass' : 'removeClass']('nav-bar-has-subheader');
    };

    // DEPRECATED, as of v1.0.0-beta14 -------
    self.changeTitle = function (val) {
        deprecatedWarning('changeTitle(val)', 'title(val)');
        self.title(val);
    };
    self.setTitle = function (val) {
        deprecatedWarning('setTitle(val)', 'title(val)');
        self.title(val);
    };
    self.getTitle = function () {
        deprecatedWarning('getTitle()', 'title()');
        return self.title();
    };
    self.back = function () {
        deprecatedWarning('back()', '$ionicHistory.goBack()');
        $ionicHistory.goBack();
    };
    self.getPreviousTitle = function () {
        deprecatedWarning('getPreviousTitle()', '$ionicHistory.backTitle()');
        $ionicHistory.goBack();
    };
    function deprecatedWarning(oldMethod, newMethod) {
        var warn = console.warn || console.log;
        warn && warn.call(console, 'navBarController.' + oldMethod + ' is deprecated, please use ' + newMethod + ' instead');
    }
    // END DEPRECATED -------


    function createNavElement(type) {
        if (navElementHtml[type]) {
            return jqLite(navElementHtml[type]);
        }
    }


    function getOnScreenHeaderBar() {
        for (var x = 0; x < headerBars.length; x++) {
            if (headerBars[x].isActive) return headerBars[x];
        }
    }


    function getOffScreenHeaderBar() {
        for (var x = 0; x < headerBars.length; x++) {
            if (!headerBars[x].isActive) return headerBars[x];
        }
    }


    function navBarAttr(ctrl, val) {
        ctrl && ionic.DomUtil.cachedAttr(ctrl.containerEle(), 'nav-bar', val);
    }

    function navSwipeAttr(val) {
        ionic.DomUtil.cachedAttr($element, 'nav-swipe', val);
    }


    $scope.$on('$destroy', function () {
        $scope.$parent.$hasHeader = false;
        $element.parent().removeData(DATA_NAV_BAR_CTRL);
        for (var x = 0; x < headerBars.length; x++) {
            headerBars[x].destroy();
        }
        $element.remove();
        $element = headerBars = null;
        deregisterInstance();
    });

} ]);

    IonicModule
.controller('$ionicNavView', [
  '$scope',
  '$element',
  '$attrs',
  '$compile',
  '$controller',
  '$ionicNavBarDelegate',
  '$ionicNavViewDelegate',
  '$ionicHistory',
  '$ionicViewSwitcher',
  '$ionicConfig',
  '$ionicScrollDelegate',
  '$ionicSideMenuDelegate',
function ($scope, $element, $attrs, $compile, $controller, $ionicNavBarDelegate, $ionicNavViewDelegate, $ionicHistory, $ionicViewSwitcher, $ionicConfig, $ionicScrollDelegate, $ionicSideMenuDelegate) {

    var DATA_ELE_IDENTIFIER = '$eleId';
    var DATA_DESTROY_ELE = '$destroyEle';
    var DATA_NO_CACHE = '$noCache';
    var VIEW_STATUS_ACTIVE = 'active';
    var VIEW_STATUS_CACHED = 'cached';

    var self = this;
    var direction;
    var isPrimary = false;
    var navBarDelegate;
    var activeEleId;
    var navViewAttr = $ionicViewSwitcher.navViewAttr;
    var disableRenderStartViewId, disableAnimation;

    self.scope = $scope;
    self.element = $element;

    self.init = function () {
        var navViewName = $attrs.name || '';

        // Find the details of the parent view directive (if any) and use it
        // to derive our own qualified view name, then hang our own details
        // off the DOM so child directives can find it.
        var parent = $element.parent().inheritedData('$uiView');
        var parentViewName = ((parent && parent.state) ? parent.state.name : '');
        if (navViewName.indexOf('@') < 0) navViewName = navViewName + '@' + parentViewName;

        var viewData = { name: navViewName, state: null };
        $element.data('$uiView', viewData);

        var deregisterInstance = $ionicNavViewDelegate._registerInstance(self, $attrs.delegateHandle);
        $scope.$on('$destroy', function () {
            deregisterInstance();

            // ensure no scrolls have been left frozen
            if (self.isSwipeFreeze) {
                $ionicScrollDelegate.freezeAllScrolls(false);
            }
        });

        $scope.$on('$ionicHistory.deselect', self.cacheCleanup);
        $scope.$on('$ionicTabs.top', onTabsTop);
        $scope.$on('$ionicSubheader', onBarSubheader);

        $scope.$on('$ionicTabs.beforeLeave', onTabsLeave);
        $scope.$on('$ionicTabs.afterLeave', onTabsLeave);
        $scope.$on('$ionicTabs.leave', onTabsLeave);

        ionic.Platform.ready(function () {
            if (ionic.Platform.isWebView() && ionic.Platform.isIOS()) {
                self.initSwipeBack();
            }
        });

        return viewData;
    };


    self.register = function (viewLocals) {
        var leavingView = extend({}, $ionicHistory.currentView());

        // register that a view is coming in and get info on how it should transition
        var registerData = $ionicHistory.register($scope, viewLocals);

        // update which direction
        self.update(registerData);

        // begin rendering and transitioning
        var enteringView = $ionicHistory.getViewById(registerData.viewId) || {};

        var renderStart = (disableRenderStartViewId !== registerData.viewId);
        self.render(registerData, viewLocals, enteringView, leavingView, renderStart, true);
    };


    self.update = function (registerData) {
        // always reset that this is the primary navView
        isPrimary = true;

        // remember what direction this navView should use
        // this may get updated later by a child navView
        direction = registerData.direction;

        var parentNavViewCtrl = $element.parent().inheritedData('$ionNavViewController');
        if (parentNavViewCtrl) {
            // this navView is nested inside another one
            // update the parent to use this direction and not
            // the other it originally was set to

            // inform the parent navView that it is not the primary navView
            parentNavViewCtrl.isPrimary(false);

            if (direction === 'enter' || direction === 'exit') {
                // they're entering/exiting a history
                // find parent navViewController
                parentNavViewCtrl.direction(direction);

                if (direction === 'enter') {
                    // reset the direction so this navView doesn't animate
                    // because it's parent will
                    direction = 'none';
                }
            }
        }
    };


    self.render = function (registerData, viewLocals, enteringView, leavingView, renderStart, renderEnd) {
        // register the view and figure out where it lives in the various
        // histories and nav stacks, along with how views should enter/leave
        var switcher = $ionicViewSwitcher.create(self, viewLocals, enteringView, leavingView, renderStart, renderEnd);

        // init the rendering of views for this navView directive
        switcher.init(registerData, function () {
            // the view is now compiled, in the dom and linked, now lets transition the views.
            // this uses a callback incase THIS nav-view has a nested nav-view, and after the NESTED
            // nav-view links, the NESTED nav-view would update which direction THIS nav-view should use

            // kick off the transition of views
            switcher.transition(self.direction(), registerData.enableBack, !disableAnimation);

            // reset private vars for next time
            disableRenderStartViewId = disableAnimation = null;
        });

    };


    self.beforeEnter = function (transitionData) {
        if (isPrimary) {
            // only update this nav-view's nav-bar if this is the primary nav-view
            navBarDelegate = transitionData.navBarDelegate;
            var associatedNavBarCtrl = getAssociatedNavBarCtrl();
            associatedNavBarCtrl && associatedNavBarCtrl.update(transitionData);
            navSwipeAttr('');
        }
    };


    self.activeEleId = function (eleId) {
        if (arguments.length) {
            activeEleId = eleId;
        }
        return activeEleId;
    };


    self.transitionEnd = function () {
        var viewElements = $element.children();
        var x, l, viewElement;

        for (x = 0, l = viewElements.length; x < l; x++) {
            viewElement = viewElements.eq(x);

            if (viewElement.data(DATA_ELE_IDENTIFIER) === activeEleId) {
                // this is the active element
                navViewAttr(viewElement, VIEW_STATUS_ACTIVE);

            } else if (navViewAttr(viewElement) === 'leaving' || navViewAttr(viewElement) === VIEW_STATUS_ACTIVE || navViewAttr(viewElement) === VIEW_STATUS_CACHED) {
                // this is a leaving element or was the former active element, or is an cached element
                if (viewElement.data(DATA_DESTROY_ELE) || viewElement.data(DATA_NO_CACHE)) {
                    // this element shouldn't stay cached
                    $ionicViewSwitcher.destroyViewEle(viewElement);

                } else {
                    // keep in the DOM, mark as cached
                    navViewAttr(viewElement, VIEW_STATUS_CACHED);

                    // disconnect the leaving scope
                    ionic.Utils.disconnectScope(viewElement.scope());
                }
            }
        }

        navSwipeAttr('');

        // ensure no scrolls have been left frozen
        if (self.isSwipeFreeze) {
            $ionicScrollDelegate.freezeAllScrolls(false);
        }
    };


    function onTabsLeave(ev, data) {
        var viewElements = $element.children();
        var viewElement, viewScope;

        for (var x = 0, l = viewElements.length; x < l; x++) {
            viewElement = viewElements.eq(x);
            if (navViewAttr(viewElement) == VIEW_STATUS_ACTIVE) {
                viewScope = viewElement.scope();
                viewScope && viewScope.$emit(ev.name.replace('Tabs', 'View'), data);
                viewScope && viewScope.$broadcast(ev.name.replace('Tabs', 'ParentView'), data);
                break;
            }
        }
    }


    self.cacheCleanup = function () {
        var viewElements = $element.children();
        for (var x = 0, l = viewElements.length; x < l; x++) {
            if (viewElements.eq(x).data(DATA_DESTROY_ELE)) {
                $ionicViewSwitcher.destroyViewEle(viewElements.eq(x));
            }
        }
    };


    self.clearCache = function (stateIds) {
        var viewElements = $element.children();
        var viewElement, viewScope, x, l, y, eleIdentifier;

        for (x = 0, l = viewElements.length; x < l; x++) {
            viewElement = viewElements.eq(x);

            if (stateIds) {
                eleIdentifier = viewElement.data(DATA_ELE_IDENTIFIER);

                for (y = 0; y < stateIds.length; y++) {
                    if (eleIdentifier === stateIds[y]) {
                        $ionicViewSwitcher.destroyViewEle(viewElement);
                    }
                }
                continue;
            }

            if (navViewAttr(viewElement) == VIEW_STATUS_CACHED) {
                $ionicViewSwitcher.destroyViewEle(viewElement);

            } else if (navViewAttr(viewElement) == VIEW_STATUS_ACTIVE) {
                viewScope = viewElement.scope();
                viewScope && viewScope.$broadcast('$ionicView.clearCache');
            }

        }
    };


    self.getViewElements = function () {
        return $element.children();
    };


    self.appendViewElement = function (viewEle, viewLocals) {
        // compile the entering element and get the link function
        var linkFn = $compile(viewEle);

        $element.append(viewEle);

        var viewScope = $scope.$new();

        if (viewLocals && viewLocals.$$controller) {
            viewLocals.$scope = viewScope;
            var controller = $controller(viewLocals.$$controller, viewLocals);
            if (viewLocals.$$controllerAs) {
                viewScope[viewLocals.$$controllerAs] = controller;
            }
            $element.children().data('$ngControllerController', controller);
        }

        linkFn(viewScope);

        return viewScope;
    };


    self.title = function (val) {
        var associatedNavBarCtrl = getAssociatedNavBarCtrl();
        associatedNavBarCtrl && associatedNavBarCtrl.title(val);
    };


    /**
    * @ngdoc method
    * @name $ionicNavView#enableBackButton
    * @description Enable/disable if the back button can be shown or not. For
    * example, the very first view in the navigation stack would not have a
    * back view, so the back button would be disabled.
    */
    self.enableBackButton = function (shouldEnable) {
        var associatedNavBarCtrl = getAssociatedNavBarCtrl();
        associatedNavBarCtrl && associatedNavBarCtrl.enableBackButton(shouldEnable);
    };


    /**
    * @ngdoc method
    * @name $ionicNavView#showBackButton
    * @description Show/hide the nav bar active back button. If the back button
    * is not possible this will not force the back button to show. The
    * `enableBackButton()` method handles if a back button is even possible or not.
    */
    self.showBackButton = function (shouldShow) {
        var associatedNavBarCtrl = getAssociatedNavBarCtrl();
        if (associatedNavBarCtrl) {
            if (arguments.length) {
                return associatedNavBarCtrl.showActiveBackButton(shouldShow);
            }
            return associatedNavBarCtrl.showActiveBackButton();
        }
        return true;
    };


    self.showBar = function (val) {
        var associatedNavBarCtrl = getAssociatedNavBarCtrl();
        if (associatedNavBarCtrl) {
            if (arguments.length) {
                return associatedNavBarCtrl.showBar(val);
            }
            return associatedNavBarCtrl.showBar();
        }
        return true;
    };


    self.isPrimary = function (val) {
        if (arguments.length) {
            isPrimary = val;
        }
        return isPrimary;
    };


    self.direction = function (val) {
        if (arguments.length) {
            direction = val;
        }
        return direction;
    };


    self.initSwipeBack = function () {
        var swipeBackHitWidth = $ionicConfig.views.swipeBackHitWidth();
        var viewTransition, associatedNavBarCtrl, backView;
        var deregDragStart, deregDrag, deregRelease;
        var windowWidth, startDragX, dragPoints;
        var cancelData = {};

        function onDragStart(ev) {
            if (!isPrimary || !$ionicConfig.views.swipeBackEnabled() || $ionicSideMenuDelegate.isOpenRight()) return;


            startDragX = getDragX(ev);
            if (startDragX > swipeBackHitWidth) return;

            backView = $ionicHistory.backView();

            var currentView = $ionicHistory.currentView();

            if (!backView || backView.historyId !== currentView.historyId || currentView.canSwipeBack === false) return;

            if (!windowWidth) windowWidth = window.innerWidth;

            self.isSwipeFreeze = $ionicScrollDelegate.freezeAllScrolls(true);

            var registerData = {
                direction: 'back'
            };

            dragPoints = [];

            cancelData = {
                showBar: self.showBar(),
                showBackButton: self.showBackButton()
            };

            var switcher = $ionicViewSwitcher.create(self, registerData, backView, currentView, true, false);
            switcher.loadViewElements(registerData);
            switcher.render(registerData);

            viewTransition = switcher.transition('back', $ionicHistory.enabledBack(backView), true);

            associatedNavBarCtrl = getAssociatedNavBarCtrl();

            deregDrag = ionic.onGesture('drag', onDrag, $element[0]);
            deregRelease = ionic.onGesture('release', onRelease, $element[0]);
        }

        function onDrag(ev) {
            if (isPrimary && viewTransition) {
                var dragX = getDragX(ev);

                dragPoints.push({
                    t: Date.now(),
                    x: dragX
                });

                if (dragX >= windowWidth - 15) {
                    onRelease(ev);

                } else {
                    var step = Math.min(Math.max(getSwipeCompletion(dragX), 0), 1);
                    viewTransition.run(step);
                    associatedNavBarCtrl && associatedNavBarCtrl.activeTransition && associatedNavBarCtrl.activeTransition.run(step);
                }

            }
        }

        function onRelease(ev) {
            if (isPrimary && viewTransition && dragPoints && dragPoints.length > 1) {

                var now = Date.now();
                var releaseX = getDragX(ev);
                var startDrag = dragPoints[dragPoints.length - 1];

                for (var x = dragPoints.length - 2; x >= 0; x--) {
                    if (now - startDrag.t > 200) {
                        break;
                    }
                    startDrag = dragPoints[x];
                }

                var isSwipingRight = (releaseX >= dragPoints[dragPoints.length - 2].x);
                var releaseSwipeCompletion = getSwipeCompletion(releaseX);
                var velocity = Math.abs(startDrag.x - releaseX) / (now - startDrag.t);

                // private variables because ui-router has no way to pass custom data using $state.go
                disableRenderStartViewId = backView.viewId;
                disableAnimation = (releaseSwipeCompletion < 0.03 || releaseSwipeCompletion > 0.97);

                if (isSwipingRight && (releaseSwipeCompletion > 0.5 || velocity > 0.1)) {
                    // complete view transition on release
                    var speed = (velocity > 0.5 || velocity < 0.05 || releaseX > windowWidth - 45) ? 'fast' : 'slow';
                    navSwipeAttr(disableAnimation ? '' : speed);
                    backView.go();
                    associatedNavBarCtrl && associatedNavBarCtrl.activeTransition && associatedNavBarCtrl.activeTransition.complete(!disableAnimation, speed);

                } else {
                    // cancel view transition on release
                    navSwipeAttr(disableAnimation ? '' : 'fast');
                    disableRenderStartViewId = null;
                    viewTransition.cancel(!disableAnimation);
                    associatedNavBarCtrl && associatedNavBarCtrl.activeTransition && associatedNavBarCtrl.activeTransition.cancel(!disableAnimation, 'fast', cancelData);
                    disableAnimation = null;
                }

            }

            ionic.offGesture(deregDrag, 'drag', onDrag);
            ionic.offGesture(deregRelease, 'release', onRelease);

            windowWidth = viewTransition = dragPoints = null;

            self.isSwipeFreeze = $ionicScrollDelegate.freezeAllScrolls(false);
        }

        function getDragX(ev) {
            return ionic.tap.pointerCoord(ev.gesture.srcEvent).x;
        }

        function getSwipeCompletion(dragX) {
            return (dragX - startDragX) / windowWidth;
        }

        deregDragStart = ionic.onGesture('dragstart', onDragStart, $element[0]);

        $scope.$on('$destroy', function () {
            ionic.offGesture(deregDragStart, 'dragstart', onDragStart);
            ionic.offGesture(deregDrag, 'drag', onDrag);
            ionic.offGesture(deregRelease, 'release', onRelease);
            self.element = viewTransition = associatedNavBarCtrl = null;
        });
    };


    function navSwipeAttr(val) {
        ionic.DomUtil.cachedAttr($element, 'nav-swipe', val);
    }


    function onTabsTop(ev, isTabsTop) {
        var associatedNavBarCtrl = getAssociatedNavBarCtrl();
        associatedNavBarCtrl && associatedNavBarCtrl.hasTabsTop(isTabsTop);
    }

    function onBarSubheader(ev, isBarSubheader) {
        var associatedNavBarCtrl = getAssociatedNavBarCtrl();
        associatedNavBarCtrl && associatedNavBarCtrl.hasBarSubheader(isBarSubheader);
    }

    function getAssociatedNavBarCtrl() {
        if (navBarDelegate) {
            for (var x = 0; x < $ionicNavBarDelegate._instances.length; x++) {
                if ($ionicNavBarDelegate._instances[x].$$delegateHandle == navBarDelegate) {
                    return $ionicNavBarDelegate._instances[x];
                }
            }
        }
        return $element.inheritedData('$ionNavBarController');
    }

} ]);

    IonicModule
.controller('$ionicRefresher', [
  '$scope',
  '$attrs',
  '$element',
  '$ionicBind',
  '$timeout',
  function ($scope, $attrs, $element, $ionicBind, $timeout) {
      var self = this,
        isDragging = false,
        isOverscrolling = false,
        dragOffset = 0,
        lastOverscroll = 0,
        ptrThreshold = 60,
        activated = false,
        scrollTime = 500,
        startY = null,
        deltaY = null,
        canOverscroll = true,
        scrollParent,
        scrollChild;

      if (!isDefined($attrs.pullingIcon)) {
          $attrs.$set('pullingIcon', 'ion-android-arrow-down');
      }

      $scope.showSpinner = !isDefined($attrs.refreshingIcon) && $attrs.spinner != 'none';

      $scope.showIcon = isDefined($attrs.refreshingIcon);

      $ionicBind($scope, $attrs, {
          pullingIcon: '@',
          pullingText: '@',
          refreshingIcon: '@',
          refreshingText: '@',
          spinner: '@',
          disablePullingRotation: '@',
          $onRefresh: '&onRefresh',
          $onPulling: '&onPulling'
      });

      function handleMousedown(e) {
          e.touches = e.touches || [{
              screenX: e.screenX,
              screenY: e.screenY
          }];
          // Mouse needs this
          startY = Math.floor(e.touches[0].screenY);
      }

      function handleTouchstart(e) {
          e.touches = e.touches || [{
              screenX: e.screenX,
              screenY: e.screenY
          }];

          startY = e.touches[0].screenY;
      }

      function handleTouchend() {
          // reset Y
          startY = null;
          // if this wasn't an overscroll, get out immediately
          if (!canOverscroll && !isDragging) {
              return;
          }
          // the user has overscrolled but went back to native scrolling
          if (!isDragging) {
              dragOffset = 0;
              isOverscrolling = false;
              setScrollLock(false);
          } else {
              isDragging = false;
              dragOffset = 0;

              // the user has scroll far enough to trigger a refresh
              if (lastOverscroll > ptrThreshold) {
                  start();
                  scrollTo(ptrThreshold, scrollTime);

                  // the user has overscrolled but not far enough to trigger a refresh
              } else {
                  scrollTo(0, scrollTime, deactivate);
                  isOverscrolling = false;
              }
          }
      }

      function handleTouchmove(e) {
          e.touches = e.touches || [{
              screenX: e.screenX,
              screenY: e.screenY
          }];

          // Force mouse events to have had a down event first
          if (!startY && e.type == 'mousemove') {
              return;
          }

          // if multitouch or regular scroll event, get out immediately
          if (!canOverscroll || e.touches.length > 1) {
              return;
          }
          //if this is a new drag, keep track of where we start
          if (startY === null) {
              startY = e.touches[0].screenY;
          }

          deltaY = e.touches[0].screenY - startY;

          // how far have we dragged so far?
          // kitkat fix for touchcancel events http://updates.html5rocks.com/2014/05/A-More-Compatible-Smoother-Touch
          // Only do this if we're not on crosswalk
          if (ionic.Platform.isAndroid() && ionic.Platform.version() === 4.4 && !ionic.Platform.isCrosswalk() && scrollParent.scrollTop === 0 && deltaY > 0) {
              isDragging = true;
              e.preventDefault();
          }


          // if we've dragged up and back down in to native scroll territory
          if (deltaY - dragOffset <= 0 || scrollParent.scrollTop !== 0) {

              if (isOverscrolling) {
                  isOverscrolling = false;
                  setScrollLock(false);
              }

              if (isDragging) {
                  nativescroll(scrollParent, deltaY - dragOffset * -1);
              }

              // if we're not at overscroll 0 yet, 0 out
              if (lastOverscroll !== 0) {
                  overscroll(0);
              }
              return;

          } else if (deltaY > 0 && scrollParent.scrollTop === 0 && !isOverscrolling) {
              // starting overscroll, but drag started below scrollTop 0, so we need to offset the position
              dragOffset = deltaY;
          }

          // prevent native scroll events while overscrolling
          e.preventDefault();

          // if not overscrolling yet, initiate overscrolling
          if (!isOverscrolling) {
              isOverscrolling = true;
              setScrollLock(true);
          }

          isDragging = true;
          // overscroll according to the user's drag so far
          overscroll((deltaY - dragOffset) / 3);

          // update the icon accordingly
          if (!activated && lastOverscroll > ptrThreshold) {
              activated = true;
              ionic.requestAnimationFrame(activate);

          } else if (activated && lastOverscroll < ptrThreshold) {
              activated = false;
              ionic.requestAnimationFrame(deactivate);
          }
      }

      function handleScroll(e) {
          // canOverscrol is used to greatly simplify the drag handler during normal scrolling
          canOverscroll = (e.target.scrollTop === 0) || isDragging;
      }

      function overscroll(val) {
          scrollChild.style[ionic.CSS.TRANSFORM] = 'translate3d(0px, ' + val + 'px, 0px)';
          lastOverscroll = val;
      }

      function nativescroll(target, newScrollTop) {
          // creates a scroll event that bubbles, can be cancelled, and with its view
          // and detail property initialized to window and 1, respectively
          target.scrollTop = newScrollTop;
          var e = document.createEvent("UIEvents");
          e.initUIEvent("scroll", true, true, window, 1);
          target.dispatchEvent(e);
      }

      function setScrollLock(enabled) {
          // set the scrollbar to be position:fixed in preparation to overscroll
          // or remove it so the app can be natively scrolled
          if (enabled) {
              ionic.requestAnimationFrame(function () {
                  scrollChild.classList.add('overscroll');
                  show();
              });

          } else {
              ionic.requestAnimationFrame(function () {
                  scrollChild.classList.remove('overscroll');
                  hide();
                  deactivate();
              });
          }
      }

      $scope.$on('scroll.refreshComplete', function () {
          // prevent the complete from firing before the scroll has started
          $timeout(function () {

              ionic.requestAnimationFrame(tail);

              // scroll back to home during tail animation
              scrollTo(0, scrollTime, deactivate);

              // return to native scrolling after tail animation has time to finish
              $timeout(function () {

                  if (isOverscrolling) {
                      isOverscrolling = false;
                      setScrollLock(false);
                  }

              }, scrollTime);

          }, scrollTime);
      });

      function scrollTo(Y, duration, callback) {
          // scroll animation loop w/ easing
          // credit https://gist.github.com/dezinezync/5487119
          var start = Date.now(),
          from = lastOverscroll;

          if (from === Y) {
              callback();
              return; /* Prevent scrolling to the Y point if already there */
          }

          // decelerating to zero velocity
          function easeOutCubic(t) {
              return (--t) * t * t + 1;
          }

          // scroll loop
          function scroll() {
              var currentTime = Date.now(),
          time = Math.min(1, ((currentTime - start) / duration)),
              // where .5 would be 50% of time on a linear scale easedT gives a
              // fraction based on the easing method
          easedT = easeOutCubic(time);

              overscroll(Math.floor((easedT * (Y - from)) + from));

              if (time < 1) {
                  ionic.requestAnimationFrame(scroll);

              } else {

                  if (Y < 5 && Y > -5) {
                      isOverscrolling = false;
                      setScrollLock(false);
                  }

                  callback && callback();
              }
          }

          // start scroll loop
          ionic.requestAnimationFrame(scroll);
      }


      var touchStartEvent, touchMoveEvent, touchEndEvent;
      if (window.navigator.pointerEnabled) {
          touchStartEvent = 'pointerdown';
          touchMoveEvent = 'pointermove';
          touchEndEvent = 'pointerup';
      } else if (window.navigator.msPointerEnabled) {
          touchStartEvent = 'MSPointerDown';
          touchMoveEvent = 'MSPointerMove';
          touchEndEvent = 'MSPointerUp';
      } else {
          touchStartEvent = 'touchstart';
          touchMoveEvent = 'touchmove';
          touchEndEvent = 'touchend';
      }

      self.init = function () {
          scrollParent = $element.parent().parent()[0];
          scrollChild = $element.parent()[0];

          if (!scrollParent || !scrollParent.classList.contains('ionic-scroll') ||
        !scrollChild || !scrollChild.classList.contains('scroll')) {
              throw new Error('Refresher must be immediate child of ion-content or ion-scroll');
          }


          ionic.on(touchStartEvent, handleTouchstart, scrollChild);
          ionic.on(touchMoveEvent, handleTouchmove, scrollChild);
          ionic.on(touchEndEvent, handleTouchend, scrollChild);
          ionic.on('mousedown', handleMousedown, scrollChild);
          ionic.on('mousemove', handleTouchmove, scrollChild);
          ionic.on('mouseup', handleTouchend, scrollChild);
          ionic.on('scroll', handleScroll, scrollParent);

          // cleanup when done
          $scope.$on('$destroy', destroy);
      };

      function destroy() {
          if (scrollChild) {
              ionic.off(touchStartEvent, handleTouchstart, scrollChild);
              ionic.off(touchMoveEvent, handleTouchmove, scrollChild);
              ionic.off(touchEndEvent, handleTouchend, scrollChild);
              ionic.off('mousedown', handleMousedown, scrollChild);
              ionic.off('mousemove', handleTouchmove, scrollChild);
              ionic.off('mouseup', handleTouchend, scrollChild);
          }
          if (scrollParent) {
              ionic.off('scroll', handleScroll, scrollParent);
          }
          scrollParent = null;
          scrollChild = null;
      }

      // DOM manipulation and broadcast methods shared by JS and Native Scrolling
      // getter used by JS Scrolling
      self.getRefresherDomMethods = function () {
          return {
              activate: activate,
              deactivate: deactivate,
              start: start,
              show: show,
              hide: hide,
              tail: tail
          };
      };

      function activate() {
          $element[0].classList.add('active');
          $scope.$onPulling();
      }

      function deactivate() {
          // give tail 150ms to finish
          $timeout(function () {
              // deactivateCallback
              $element.removeClass('active refreshing refreshing-tail');
              if (activated) activated = false;
          }, 150);
      }

      function start() {
          // startCallback
          $element[0].classList.add('refreshing');
          var q = $scope.$onRefresh();

          if (q && q.then) {
              q['finally'](function () {
                  $scope.$broadcast('scroll.refreshComplete');
              });
          }
      }

      function show() {
          // showCallback
          $element[0].classList.remove('invisible');
      }

      function hide() {
          // showCallback
          $element[0].classList.add('invisible');
      }

      function tail() {
          // tailCallback
          $element[0].classList.add('refreshing-tail');
      }

      // for testing
      self.__handleTouchmove = handleTouchmove;
      self.__getScrollChild = function () { return scrollChild; };
      self.__getScrollParent = function () { return scrollParent; };
  }
]);

    /**
    * @private
    */
    IonicModule

.controller('$ionicScroll', [
  '$scope',
  'scrollViewOptions',
  '$timeout',
  '$window',
  '$location',
  '$document',
  '$ionicScrollDelegate',
  '$ionicHistory',
function ($scope,
         scrollViewOptions,
         $timeout,
         $window,
         $location,
         $document,
         $ionicScrollDelegate,
         $ionicHistory) {

    var self = this;
    // for testing
    self.__timeout = $timeout;

    self._scrollViewOptions = scrollViewOptions; //for testing
    self.isNative = function () {
        return !!scrollViewOptions.nativeScrolling;
    };

    var element = self.element = scrollViewOptions.el;
    var $element = self.$element = jqLite(element);
    var scrollView;
    if (self.isNative()) {
        scrollView = self.scrollView = new ionic.views.ScrollNative(scrollViewOptions);
    } else {
        scrollView = self.scrollView = new ionic.views.Scroll(scrollViewOptions);
    }


    //Attach self to element as a controller so other directives can require this controller
    //through `require: '$ionicScroll'
    //Also attach to parent so that sibling elements can require this
    ($element.parent().length ? $element.parent() : $element)
    .data('$$ionicScrollController', self);

    var deregisterInstance = $ionicScrollDelegate._registerInstance(
    self, scrollViewOptions.delegateHandle, function () {
        return $ionicHistory.isActiveScope($scope);
    }
  );

    if (!isDefined(scrollViewOptions.bouncing)) {
        ionic.Platform.ready(function () {
            if (scrollView && scrollView.options) {
                scrollView.options.bouncing = true;
                if (ionic.Platform.isAndroid()) {
                    // No bouncing by default on Android
                    scrollView.options.bouncing = false;
                    // Faster scroll decel
                    scrollView.options.deceleration = 0.95;
                }
            }
        });
    }

    var resize = angular.bind(scrollView, scrollView.resize);
    angular.element($window).on('resize', resize);

    var scrollFunc = function (e) {
        var detail = (e.originalEvent || e).detail || {};
        $scope.$onScroll && $scope.$onScroll({
            event: e,
            scrollTop: detail.scrollTop || 0,
            scrollLeft: detail.scrollLeft || 0
        });
    };

    $element.on('scroll', scrollFunc);

    $scope.$on('$destroy', function () {
        deregisterInstance();
        scrollView && scrollView.__cleanup && scrollView.__cleanup();
        angular.element($window).off('resize', resize);
        if ($element) {
            $element.off('scroll', scrollFunc);
        }
        if (self._scrollViewOptions) {
            self._scrollViewOptions.el = null;
        }
        if (scrollViewOptions) {
            scrollViewOptions.el = null;
        }

        scrollView = self.scrollView = scrollViewOptions = self._scrollViewOptions = element = self.$element = $element = null;
    });

    $timeout(function () {
        scrollView && scrollView.run && scrollView.run();
    });

    self.getScrollView = function () {
        return scrollView;
    };

    self.getScrollPosition = function () {
        return scrollView.getValues();
    };

    self.resize = function () {
        return $timeout(resize, 0, false).then(function () {
            $element && $element.triggerHandler('scroll-resize');
        });
    };

    self.scrollTop = function (shouldAnimate) {
        self.resize().then(function () {
            if (!scrollView) {
                return;
            }
            scrollView.scrollTo(0, 0, !!shouldAnimate);
        });
    };

    self.scrollBottom = function (shouldAnimate) {
        self.resize().then(function () {
            if (!scrollView) {
                return;
            }
            var max = scrollView.getScrollMax();
            scrollView.scrollTo(max.left, max.top, !!shouldAnimate);
        });
    };

    self.scrollTo = function (left, top, shouldAnimate) {
        self.resize().then(function () {
            if (!scrollView) {
                return;
            }
            scrollView.scrollTo(left, top, !!shouldAnimate);
        });
    };

    self.zoomTo = function (zoom, shouldAnimate, originLeft, originTop) {
        self.resize().then(function () {
            if (!scrollView) {
                return;
            }
            scrollView.zoomTo(zoom, !!shouldAnimate, originLeft, originTop);
        });
    };

    self.zoomBy = function (zoom, shouldAnimate, originLeft, originTop) {
        self.resize().then(function () {
            if (!scrollView) {
                return;
            }
            scrollView.zoomBy(zoom, !!shouldAnimate, originLeft, originTop);
        });
    };

    self.scrollBy = function (left, top, shouldAnimate) {
        self.resize().then(function () {
            if (!scrollView) {
                return;
            }
            scrollView.scrollBy(left, top, !!shouldAnimate);
        });
    };

    self.anchorScroll = function (shouldAnimate) {
        self.resize().then(function () {
            if (!scrollView) {
                return;
            }
            var hash = $location.hash();
            var elm = hash && $document[0].getElementById(hash);
            if (!(hash && elm)) {
                scrollView.scrollTo(0, 0, !!shouldAnimate);
                return;
            }
            var curElm = elm;
            var scrollLeft = 0, scrollTop = 0;
            do {
                if (curElm !== null) scrollLeft += curElm.offsetLeft;
                if (curElm !== null) scrollTop += curElm.offsetTop;
                curElm = curElm.offsetParent;
            } while (curElm.attributes != self.element.attributes && curElm.offsetParent);
            scrollView.scrollTo(scrollLeft, scrollTop, !!shouldAnimate);
        });
    };

    self.freezeScroll = scrollView.freeze;
    self.freezeScrollShut = scrollView.freezeShut;

    self.freezeAllScrolls = function (shouldFreeze) {
        for (var i = 0; i < $ionicScrollDelegate._instances.length; i++) {
            $ionicScrollDelegate._instances[i].freezeScroll(shouldFreeze);
        }
    };


    /**
    * @private
    */
    self._setRefresher = function (refresherScope, refresherElement, refresherMethods) {
        self.refresher = refresherElement;
        var refresherHeight = self.refresher.clientHeight || 60;
        scrollView.activatePullToRefresh(
      refresherHeight,
      refresherMethods
    );
    };

} ]);

    IonicModule
.controller('$ionicSideMenus', [
  '$scope',
  '$attrs',
  '$ionicSideMenuDelegate',
  '$ionicPlatform',
  '$ionicBody',
  '$ionicHistory',
  '$ionicScrollDelegate',
  'IONIC_BACK_PRIORITY',
  '$rootScope',
function ($scope, $attrs, $ionicSideMenuDelegate, $ionicPlatform, $ionicBody, $ionicHistory, $ionicScrollDelegate, IONIC_BACK_PRIORITY, $rootScope) {
    var self = this;
    var rightShowing, leftShowing, isDragging;
    var startX, lastX, offsetX, isAsideExposed;
    var enableMenuWithBackViews = true;

    self.$scope = $scope;

    self.initialize = function (options) {
        self.left = options.left;
        self.right = options.right;
        self.setContent(options.content);
        self.dragThresholdX = options.dragThresholdX || 10;
        $ionicHistory.registerHistory(self.$scope);
    };

    /**
    * Set the content view controller if not passed in the constructor options.
    *
    * @param {object} content
    */
    self.setContent = function (content) {
        if (content) {
            self.content = content;

            self.content.onDrag = function (e) {
                self._handleDrag(e);
            };

            self.content.endDrag = function (e) {
                self._endDrag(e);
            };
        }
    };

    self.isOpenLeft = function () {
        return self.getOpenAmount() > 0;
    };

    self.isOpenRight = function () {
        return self.getOpenAmount() < 0;
    };

    /**
    * Toggle the left menu to open 100%
    */
    self.toggleLeft = function (shouldOpen) {
        if (isAsideExposed || !self.left.isEnabled) return;
        var openAmount = self.getOpenAmount();
        if (arguments.length === 0) {
            shouldOpen = openAmount <= 0;
        }
        self.content.enableAnimation();
        if (!shouldOpen) {
            self.openPercentage(0);
            $rootScope.$emit('$ionicSideMenuClose', 'left');
        } else {
            self.openPercentage(100);
            $rootScope.$emit('$ionicSideMenuOpen', 'left');
        }
    };

    /**
    * Toggle the right menu to open 100%
    */
    self.toggleRight = function (shouldOpen) {
        if (isAsideExposed || !self.right.isEnabled) return;
        var openAmount = self.getOpenAmount();
        if (arguments.length === 0) {
            shouldOpen = openAmount >= 0;
        }
        self.content.enableAnimation();
        if (!shouldOpen) {
            self.openPercentage(0);
            $rootScope.$emit('$ionicSideMenuClose', 'right');
        } else {
            self.openPercentage(-100);
            $rootScope.$emit('$ionicSideMenuOpen', 'right');
        }
    };

    self.toggle = function (side) {
        if (side == 'right') {
            self.toggleRight();
        } else {
            self.toggleLeft();
        }
    };

    /**
    * Close all menus.
    */
    self.close = function () {
        self.openPercentage(0);
        $rootScope.$emit('$ionicSideMenuClose', 'left');
        $rootScope.$emit('$ionicSideMenuClose', 'right');
    };

    /**
    * @return {float} The amount the side menu is open, either positive or negative for left (positive), or right (negative)
    */
    self.getOpenAmount = function () {
        return self.content && self.content.getTranslateX() || 0;
    };

    /**
    * @return {float} The ratio of open amount over menu width. For example, a
    * menu of width 100 open 50 pixels would be open 50% or a ratio of 0.5. Value is negative
    * for right menu.
    */
    self.getOpenRatio = function () {
        var amount = self.getOpenAmount();
        if (amount >= 0) {
            return amount / self.left.width;
        }
        return amount / self.right.width;
    };

    self.isOpen = function () {
        return self.getOpenAmount() !== 0;
    };

    /**
    * @return {float} The percentage of open amount over menu width. For example, a
    * menu of width 100 open 50 pixels would be open 50%. Value is negative
    * for right menu.
    */
    self.getOpenPercentage = function () {
        return self.getOpenRatio() * 100;
    };

    /**
    * Open the menu with a given percentage amount.
    * @param {float} percentage The percentage (positive or negative for left/right) to open the menu.
    */
    self.openPercentage = function (percentage) {
        var p = percentage / 100;

        if (self.left && percentage >= 0) {
            self.openAmount(self.left.width * p);
        } else if (self.right && percentage < 0) {
            self.openAmount(self.right.width * p);
        }

        // add the CSS class "menu-open" if the percentage does not
        // equal 0, otherwise remove the class from the body element
        $ionicBody.enableClass((percentage !== 0), 'menu-open');

        self.content.setCanScroll(percentage == 0);
    };

    /*
    function freezeAllScrolls(shouldFreeze) {
    if (shouldFreeze && !self.isScrollFreeze) {
    $ionicScrollDelegate.freezeAllScrolls(shouldFreeze);

    } else if (!shouldFreeze && self.isScrollFreeze) {
    $ionicScrollDelegate.freezeAllScrolls(false);
    }
    self.isScrollFreeze = shouldFreeze;
    }
    */

    /**
    * Open the menu the given pixel amount.
    * @param {float} amount the pixel amount to open the menu. Positive value for left menu,
    * negative value for right menu (only one menu will be visible at a time).
    */
    self.openAmount = function (amount) {
        var maxLeft = self.left && self.left.width || 0;
        var maxRight = self.right && self.right.width || 0;

        // Check if we can move to that side, depending if the left/right panel is enabled
        if (!(self.left && self.left.isEnabled) && amount > 0) {
            self.content.setTranslateX(0);
            return;
        }

        if (!(self.right && self.right.isEnabled) && amount < 0) {
            self.content.setTranslateX(0);
            return;
        }

        if (leftShowing && amount > maxLeft) {
            self.content.setTranslateX(maxLeft);
            return;
        }

        if (rightShowing && amount < -maxRight) {
            self.content.setTranslateX(-maxRight);
            return;
        }

        self.content.setTranslateX(amount);

        leftShowing = amount > 0;
        rightShowing = amount < 0;

        if (amount > 0) {
            // Push the z-index of the right menu down
            self.right && self.right.pushDown && self.right.pushDown();
            // Bring the z-index of the left menu up
            self.left && self.left.bringUp && self.left.bringUp();
        } else {
            // Bring the z-index of the right menu up
            self.right && self.right.bringUp && self.right.bringUp();
            // Push the z-index of the left menu down
            self.left && self.left.pushDown && self.left.pushDown();
        }
    };

    /**
    * Given an event object, find the final resting position of this side
    * menu. For example, if the user "throws" the content to the right and
    * releases the touch, the left menu should snap open (animated, of course).
    *
    * @param {Event} e the gesture event to use for snapping
    */
    self.snapToRest = function (e) {
        // We want to animate at the end of this
        self.content.enableAnimation();
        isDragging = false;

        // Check how much the panel is open after the drag, and
        // what the drag velocity is
        var ratio = self.getOpenRatio();

        if (ratio === 0) {
            // Just to be safe
            self.openPercentage(0);
            return;
        }

        var velocityThreshold = 0.3;
        var velocityX = e.gesture.velocityX;
        var direction = e.gesture.direction;

        // Going right, less than half, too slow (snap back)
        if (ratio > 0 && ratio < 0.5 && direction == 'right' && velocityX < velocityThreshold) {
            self.openPercentage(0);
        }

        // Going left, more than half, too slow (snap back)
        else if (ratio > 0.5 && direction == 'left' && velocityX < velocityThreshold) {
            self.openPercentage(100);
        }

        // Going left, less than half, too slow (snap back)
        else if (ratio < 0 && ratio > -0.5 && direction == 'left' && velocityX < velocityThreshold) {
            self.openPercentage(0);
        }

        // Going right, more than half, too slow (snap back)
        else if (ratio < 0.5 && direction == 'right' && velocityX < velocityThreshold) {
            self.openPercentage(-100);
        }

        // Going right, more than half, or quickly (snap open)
        else if (direction == 'right' && ratio >= 0 && (ratio >= 0.5 || velocityX > velocityThreshold)) {
            self.openPercentage(100);
        }

        // Going left, more than half, or quickly (span open)
        else if (direction == 'left' && ratio <= 0 && (ratio <= -0.5 || velocityX > velocityThreshold)) {
            self.openPercentage(-100);
        }

        // Snap back for safety
        else {
            self.openPercentage(0);
        }
    };

    self.enableMenuWithBackViews = function (val) {
        if (arguments.length) {
            enableMenuWithBackViews = !!val;
        }
        return enableMenuWithBackViews;
    };

    self.isAsideExposed = function () {
        return !!isAsideExposed;
    };

    self.exposeAside = function (shouldExposeAside) {
        if (!(self.left && self.left.isEnabled) && !(self.right && self.right.isEnabled)) return;
        self.close();

        isAsideExposed = shouldExposeAside;
        if ((self.left && self.left.isEnabled) && (self.right && self.right.isEnabled)) {
            self.content.setMarginLeftAndRight(isAsideExposed ? self.left.width : 0, isAsideExposed ? self.right.width : 0);
        } else if (self.left && self.left.isEnabled) {
            // set the left marget width if it should be exposed
            // otherwise set false so there's no left margin
            self.content.setMarginLeft(isAsideExposed ? self.left.width : 0);
        } else if (self.right && self.right.isEnabled) {
            self.content.setMarginRight(isAsideExposed ? self.right.width : 0);
        }
        self.$scope.$emit('$ionicExposeAside', isAsideExposed);
    };

    self.activeAsideResizing = function (isResizing) {
        $ionicBody.enableClass(isResizing, 'aside-resizing');
    };

    // End a drag with the given event
    self._endDrag = function (e) {
        if (isAsideExposed) return;

        if (isDragging) {
            self.snapToRest(e);
        }
        startX = null;
        lastX = null;
        offsetX = null;
    };

    // Handle a drag event
    self._handleDrag = function (e) {
        if (isAsideExposed || !$scope.dragContent) return;

        // If we don't have start coords, grab and store them
        if (!startX) {
            startX = e.gesture.touches[0].pageX;
            lastX = startX;
        } else {
            // Grab the current tap coords
            lastX = e.gesture.touches[0].pageX;
        }

        // Calculate difference from the tap points
        if (!isDragging && Math.abs(lastX - startX) > self.dragThresholdX) {
            // if the difference is greater than threshold, start dragging using the current
            // point as the starting point
            startX = lastX;

            isDragging = true;
            // Initialize dragging
            self.content.disableAnimation();
            offsetX = self.getOpenAmount();
        }

        if (isDragging) {
            self.openAmount(offsetX + (lastX - startX));
            //self.content.setCanScroll(false);
        }
    };

    self.canDragContent = function (canDrag) {
        if (arguments.length) {
            $scope.dragContent = !!canDrag;
        }
        return $scope.dragContent;
    };

    self.edgeThreshold = 25;
    self.edgeThresholdEnabled = false;
    self.edgeDragThreshold = function (value) {
        if (arguments.length) {
            if (isNumber(value) && value > 0) {
                self.edgeThreshold = value;
                self.edgeThresholdEnabled = true;
            } else {
                self.edgeThresholdEnabled = !!value;
            }
        }
        return self.edgeThresholdEnabled;
    };

    self.isDraggableTarget = function (e) {
        //Only restrict edge when sidemenu is closed and restriction is enabled
        var shouldOnlyAllowEdgeDrag = self.edgeThresholdEnabled && !self.isOpen();
        var startX = e.gesture.startEvent && e.gesture.startEvent.center &&
      e.gesture.startEvent.center.pageX;

        var dragIsWithinBounds = !shouldOnlyAllowEdgeDrag ||
      startX <= self.edgeThreshold ||
      startX >= self.content.element.offsetWidth - self.edgeThreshold;

        var backView = $ionicHistory.backView();
        var menuEnabled = enableMenuWithBackViews ? true : !backView;
        if (!menuEnabled) {
            var currentView = $ionicHistory.currentView() || {};
            return (dragIsWithinBounds && (backView.historyId !== currentView.historyId));
        }

        return ($scope.dragContent || self.isOpen()) &&
      dragIsWithinBounds &&
      !e.gesture.srcEvent.defaultPrevented &&
      menuEnabled &&
      !e.target.tagName.match(/input|textarea|select|object|embed/i) &&
      !e.target.isContentEditable &&
      !(e.target.dataset ? e.target.dataset.preventScroll : e.target.getAttribute('data-prevent-scroll') == 'true');
    };

    $scope.sideMenuContentTranslateX = 0;

    var deregisterBackButtonAction = noop;
    var closeSideMenu = angular.bind(self, self.close);

    $scope.$watch(function () {
        return self.getOpenAmount() !== 0;
    }, function (isOpen) {
        deregisterBackButtonAction();
        if (isOpen) {
            deregisterBackButtonAction = $ionicPlatform.registerBackButtonAction(
        closeSideMenu,
        IONIC_BACK_PRIORITY.sideMenu
      );
        }
    });

    var deregisterInstance = $ionicSideMenuDelegate._registerInstance(
    self, $attrs.delegateHandle, function () {
        return $ionicHistory.isActiveScope($scope);
    }
  );

    $scope.$on('$destroy', function () {
        deregisterInstance();
        deregisterBackButtonAction();
        self.$scope = null;
        if (self.content) {
            self.content.setCanScroll(true);
            self.content.element = null;
            self.content = null;
        }
    });

    self.initialize({
        left: {
            width: 275
        },
        right: {
            width: 275
        }
    });

} ]);

    (function (ionic) {

        var TRANSLATE32 = 'translate(32,32)';
        var STROKE_OPACITY = 'stroke-opacity';
        var ROUND = 'round';
        var INDEFINITE = 'indefinite';
        var DURATION = '750ms';
        var NONE = 'none';
        var SHORTCUTS = {
            a: 'animate',
            an: 'attributeName',
            at: 'animateTransform',
            c: 'circle',
            da: 'stroke-dasharray',
            os: 'stroke-dashoffset',
            f: 'fill',
            lc: 'stroke-linecap',
            rc: 'repeatCount',
            sw: 'stroke-width',
            t: 'transform',
            v: 'values'
        };

        var SPIN_ANIMATION = {
            v: '0,32,32;360,32,32',
            an: 'transform',
            type: 'rotate',
            rc: INDEFINITE,
            dur: DURATION
        };

        function createSvgElement(tagName, data, parent, spinnerName) {
            var ele = document.createElement(SHORTCUTS[tagName] || tagName);
            var k, x, y;

            for (k in data) {

                if (angular.isArray(data[k])) {
                    for (x = 0; x < data[k].length; x++) {
                        if (data[k][x].fn) {
                            for (y = 0; y < data[k][x].t; y++) {
                                createSvgElement(k, data[k][x].fn(y, spinnerName), ele, spinnerName);
                            }
                        } else {
                            createSvgElement(k, data[k][x], ele, spinnerName);
                        }
                    }

                } else {
                    setSvgAttribute(ele, k, data[k]);
                }
            }

            parent.appendChild(ele);
        }

        function setSvgAttribute(ele, k, v) {
            ele.setAttribute(SHORTCUTS[k] || k, v);
        }

        function animationValues(strValues, i) {
            var values = strValues.split(';');
            var back = values.slice(i);
            var front = values.slice(0, values.length - back.length);
            values = back.concat(front).reverse();
            return values.join(';') + ';' + values[0];
        }

        var IOS_SPINNER = {
            sw: 4,
            lc: ROUND,
            line: [{
                fn: function (i, spinnerName) {
                    return {
                        y1: spinnerName == 'ios' ? 17 : 12,
                        y2: spinnerName == 'ios' ? 29 : 20,
                        t: TRANSLATE32 + ' rotate(' + (30 * i + (i < 6 ? 180 : -180)) + ')',
                        a: [{
                            fn: function () {
                                return {
                                    an: STROKE_OPACITY,
                                    dur: DURATION,
                                    v: animationValues('0;.1;.15;.25;.35;.45;.55;.65;.7;.85;1', i),
                                    rc: INDEFINITE
                                };
                            },
                            t: 1
                        }]
                    };
                },
                t: 12
            }]
        };

        var spinners = {

            android: {
                c: [{
                    sw: 6,
                    da: 128,
                    os: 82,
                    r: 26,
                    cx: 32,
                    cy: 32,
                    f: NONE
                }]
            },

            ios: IOS_SPINNER,

            'ios-small': IOS_SPINNER,

            bubbles: {
                sw: 0,
                c: [{
                    fn: function (i) {
                        return {
                            cx: 24 * Math.cos(2 * Math.PI * i / 8),
                            cy: 24 * Math.sin(2 * Math.PI * i / 8),
                            t: TRANSLATE32,
                            a: [{
                                fn: function () {
                                    return {
                                        an: 'r',
                                        dur: DURATION,
                                        v: animationValues('1;2;3;4;5;6;7;8', i),
                                        rc: INDEFINITE
                                    };
                                },
                                t: 1
                            }]
                        };
                    },
                    t: 8
                }]
            },

            circles: {

                c: [{
                    fn: function (i) {
                        return {
                            r: 5,
                            cx: 24 * Math.cos(2 * Math.PI * i / 8),
                            cy: 24 * Math.sin(2 * Math.PI * i / 8),
                            t: TRANSLATE32,
                            sw: 0,
                            a: [{
                                fn: function () {
                                    return {
                                        an: 'fill-opacity',
                                        dur: DURATION,
                                        v: animationValues('.3;.3;.3;.4;.7;.85;.9;1', i),
                                        rc: INDEFINITE
                                    };
                                },
                                t: 1
                            }]
                        };
                    },
                    t: 8
                }]
            },

            crescent: {
                c: [{
                    sw: 4,
                    da: 128,
                    os: 82,
                    r: 26,
                    cx: 32,
                    cy: 32,
                    f: NONE,
                    at: [SPIN_ANIMATION]
                }]
            },

            dots: {

                c: [{
                    fn: function (i) {
                        return {
                            cx: 16 + (16 * i),
                            cy: 32,
                            sw: 0,
                            a: [{
                                fn: function () {
                                    return {
                                        an: 'fill-opacity',
                                        dur: DURATION,
                                        v: animationValues('.5;.6;.8;1;.8;.6;.5', i),
                                        rc: INDEFINITE
                                    };
                                },
                                t: 1
                            }, {
                                fn: function () {
                                    return {
                                        an: 'r',
                                        dur: DURATION,
                                        v: animationValues('4;5;6;5;4;3;3', i),
                                        rc: INDEFINITE
                                    };
                                },
                                t: 1
                            }]
                        };
                    },
                    t: 3
                }]
            },

            lines: {
                sw: 7,
                lc: ROUND,
                line: [{
                    fn: function (i) {
                        return {
                            x1: 10 + (i * 14),
                            x2: 10 + (i * 14),
                            a: [{
                                fn: function () {
                                    return {
                                        an: 'y1',
                                        dur: DURATION,
                                        v: animationValues('16;18;28;18;16', i),
                                        rc: INDEFINITE
                                    };
                                },
                                t: 1
                            }, {
                                fn: function () {
                                    return {
                                        an: 'y2',
                                        dur: DURATION,
                                        v: animationValues('48;44;36;46;48', i),
                                        rc: INDEFINITE
                                    };
                                },
                                t: 1
                            }, {
                                fn: function () {
                                    return {
                                        an: STROKE_OPACITY,
                                        dur: DURATION,
                                        v: animationValues('1;.8;.5;.4;1', i),
                                        rc: INDEFINITE
                                    };
                                },
                                t: 1
                            }]
                        };
                    },
                    t: 4
                }]
            },

            ripple: {
                f: NONE,
                'fill-rule': 'evenodd',
                sw: 3,
                circle: [{
                    fn: function (i) {
                        return {
                            cx: 32,
                            cy: 32,
                            a: [{
                                fn: function () {
                                    return {
                                        an: 'r',
                                        begin: (i * -1) + 's',
                                        dur: '2s',
                                        v: '0;24',
                                        keyTimes: '0;1',
                                        keySplines: '0.1,0.2,0.3,1',
                                        calcMode: 'spline',
                                        rc: INDEFINITE
                                    };
                                },
                                t: 1
                            }, {
                                fn: function () {
                                    return {
                                        an: STROKE_OPACITY,
                                        begin: (i * -1) + 's',
                                        dur: '2s',
                                        v: '.2;1;.2;0',
                                        rc: INDEFINITE
                                    };
                                },
                                t: 1
                            }]
                        };
                    },
                    t: 2
                }]
            },

            spiral: {
                defs: [{
                    linearGradient: [{
                        id: 'sGD',
                        gradientUnits: 'userSpaceOnUse',
                        x1: 55, y1: 46, x2: 2, y2: 46,
                        stop: [{
                            offset: 0.1,
                            class: 'stop1'
                        }, {
                            offset: 1,
                            class: 'stop2'
                        }]
                    }]
                }],
                g: [{
                    sw: 4,
                    lc: ROUND,
                    f: NONE,
                    path: [{
                        stroke: 'url(#sGD)',
                        d: 'M4,32 c0,15,12,28,28,28c8,0,16-4,21-9'
                    }, {
                        d: 'M60,32 C60,16,47.464,4,32,4S4,16,4,32'
                    }],
                    at: [SPIN_ANIMATION]
                }]
            }

        };

        var animations = {

            android: function (ele) {
                // Note that this is called as a function, not a constructor.
                var self = {};

                this.stop = false;

                var rIndex = 0;
                var rotateCircle = 0;
                var startTime;
                var svgEle = ele.querySelector('g');
                var circleEle = ele.querySelector('circle');

                function run() {
                    if (self.stop) return;

                    var v = easeInOutCubic(Date.now() - startTime, 650);
                    var scaleX = 1;
                    var translateX = 0;
                    var dasharray = (188 - (58 * v));
                    var dashoffset = (182 - (182 * v));

                    if (rIndex % 2) {
                        scaleX = -1;
                        translateX = -64;
                        dasharray = (128 - (-58 * v));
                        dashoffset = (182 * v);
                    }

                    var rotateLine = [0, -101, -90, -11, -180, 79, -270, -191][rIndex];

                    setSvgAttribute(circleEle, 'da', Math.max(Math.min(dasharray, 188), 128));
                    setSvgAttribute(circleEle, 'os', Math.max(Math.min(dashoffset, 182), 0));
                    setSvgAttribute(circleEle, 't', 'scale(' + scaleX + ',1) translate(' + translateX + ',0) rotate(' + rotateLine + ',32,32)');

                    rotateCircle += 4.1;
                    if (rotateCircle > 359) rotateCircle = 0;
                    setSvgAttribute(svgEle, 't', 'rotate(' + rotateCircle + ',32,32)');

                    if (v >= 1) {
                        rIndex++;
                        if (rIndex > 7) rIndex = 0;
                        startTime = Date.now();
                    }

                    ionic.requestAnimationFrame(run);
                }

                return function () {
                    startTime = Date.now();
                    run();
                    return self;
                };

            }

        };

        function easeInOutCubic(t, c) {
            t /= c / 2;
            if (t < 1) return 1 / 2 * t * t * t;
            t -= 2;
            return 1 / 2 * (t * t * t + 2);
        }


        IonicModule
  .controller('$ionicSpinner', [
    '$element',
    '$attrs',
    '$ionicConfig',
  function ($element, $attrs, $ionicConfig) {
      var spinnerName, anim;

      this.init = function () {
          spinnerName = $attrs.icon || $ionicConfig.spinner.icon();

          var container = document.createElement('div');
          createSvgElement('svg', {
              viewBox: '0 0 64 64',
              g: [spinners[spinnerName]]
          }, container, spinnerName);

          // Specifically for animations to work,
          // Android 4.3 and below requires the element to be
          // added as an html string, rather than dynmically
          // building up the svg element and appending it.
          $element.html(container.innerHTML);

          this.start();

          return spinnerName;
      };

      this.start = function () {
          animations[spinnerName] && (anim = animations[spinnerName]($element[0])());
      };

      this.stop = function () {
          animations[spinnerName] && (anim.stop = true);
      };

  } ]);

    })(ionic);

    IonicModule
.controller('$ionicTab', [
  '$scope',
  '$ionicHistory',
  '$attrs',
  '$location',
  '$state',
function ($scope, $ionicHistory, $attrs, $location, $state) {
    this.$scope = $scope;

    //All of these exposed for testing
    this.hrefMatchesState = function () {
        return $attrs.href && $location.path().indexOf(
      $attrs.href.replace(/^#/, '').replace(/\/$/, '')
    ) === 0;
    };
    this.srefMatchesState = function () {
        return $attrs.uiSref && $state.includes($attrs.uiSref.split('(')[0]);
    };
    this.navNameMatchesState = function () {
        return this.navViewName && $ionicHistory.isCurrentStateNavView(this.navViewName);
    };

    this.tabMatchesState = function () {
        return this.hrefMatchesState() || this.srefMatchesState() || this.navNameMatchesState();
    };
} ]);

    IonicModule
.controller('$ionicTabs', [
  '$scope',
  '$element',
  '$ionicHistory',
function ($scope, $element, $ionicHistory) {
    var self = this;
    var selectedTab = null;
    var previousSelectedTab = null;
    var selectedTabIndex;
    var isVisible = true;
    self.tabs = [];

    self.selectedIndex = function () {
        return self.tabs.indexOf(selectedTab);
    };
    self.selectedTab = function () {
        return selectedTab;
    };
    self.previousSelectedTab = function () {
        return previousSelectedTab;
    };

    self.add = function (tab) {
        $ionicHistory.registerHistory(tab);
        self.tabs.push(tab);
    };

    self.remove = function (tab) {
        var tabIndex = self.tabs.indexOf(tab);
        if (tabIndex === -1) {
            return;
        }
        //Use a field like '$tabSelected' so developers won't accidentally set it in controllers etc
        if (tab.$tabSelected) {
            self.deselect(tab);
            //Try to select a new tab if we're removing a tab
            if (self.tabs.length === 1) {
                //Do nothing if there are no other tabs to select
            } else {
                //Select previous tab if it's the last tab, else select next tab
                var newTabIndex = tabIndex === self.tabs.length - 1 ? tabIndex - 1 : tabIndex + 1;
                self.select(self.tabs[newTabIndex]);
            }
        }
        self.tabs.splice(tabIndex, 1);
    };

    self.deselect = function (tab) {
        if (tab.$tabSelected) {
            previousSelectedTab = selectedTab;
            selectedTab = selectedTabIndex = null;
            tab.$tabSelected = false;
            (tab.onDeselect || noop)();
            tab.$broadcast && tab.$broadcast('$ionicHistory.deselect');
        }
    };

    self.select = function (tab, shouldEmitEvent) {
        var tabIndex;
        if (isNumber(tab)) {
            tabIndex = tab;
            if (tabIndex >= self.tabs.length) return;
            tab = self.tabs[tabIndex];
        } else {
            tabIndex = self.tabs.indexOf(tab);
        }

        if (arguments.length === 1) {
            shouldEmitEvent = !!(tab.navViewName || tab.uiSref);
        }

        if (selectedTab && selectedTab.$historyId == tab.$historyId) {
            if (shouldEmitEvent) {
                $ionicHistory.goToHistoryRoot(tab.$historyId);
            }

        } else if (selectedTabIndex !== tabIndex) {
            forEach(self.tabs, function (tab) {
                self.deselect(tab);
            });

            selectedTab = tab;
            selectedTabIndex = tabIndex;

            if (self.$scope && self.$scope.$parent) {
                self.$scope.$parent.$activeHistoryId = tab.$historyId;
            }

            //Use a funny name like $tabSelected so the developer doesn't overwrite the var in a child scope
            tab.$tabSelected = true;
            (tab.onSelect || noop)();

            if (shouldEmitEvent) {
                $scope.$emit('$ionicHistory.change', {
                    type: 'tab',
                    tabIndex: tabIndex,
                    historyId: tab.$historyId,
                    navViewName: tab.navViewName,
                    hasNavView: !!tab.navViewName,
                    title: tab.title,
                    url: tab.href,
                    uiSref: tab.uiSref
                });
            }

            $scope.$broadcast("tabSelected", { selectedTab: tab, selectedTabIndex: tabIndex });
        }
    };

    self.hasActiveScope = function () {
        for (var x = 0; x < self.tabs.length; x++) {
            if ($ionicHistory.isActiveScope(self.tabs[x])) {
                return true;
            }
        }
        return false;
    };

    self.showBar = function (show) {
        if (arguments.length) {
            if (show) {
                $element.removeClass('tabs-item-hide');
            } else {
                $element.addClass('tabs-item-hide');
            }
            isVisible = !!show;
        }
        return isVisible;
    };
} ]);

    IonicModule
.controller('$ionicView', [
  '$scope',
  '$element',
  '$attrs',
  '$compile',
  '$rootScope',
function ($scope, $element, $attrs, $compile, $rootScope) {
    var self = this;
    var navElementHtml = {};
    var navViewCtrl;
    var navBarDelegateHandle;
    var hasViewHeaderBar;
    var deregisters = [];
    var viewTitle;

    var deregIonNavBarInit = $scope.$on('ionNavBar.init', function (ev, delegateHandle) {
        // this view has its own ion-nav-bar, remember the navBarDelegateHandle for this view
        ev.stopPropagation();
        navBarDelegateHandle = delegateHandle;
    });


    self.init = function () {
        deregIonNavBarInit();

        var modalCtrl = $element.inheritedData('$ionModalController');
        navViewCtrl = $element.inheritedData('$ionNavViewController');

        // don't bother if inside a modal or there's no parent navView
        if (!navViewCtrl || modalCtrl) return;

        // add listeners for when this view changes
        $scope.$on('$ionicView.beforeEnter', self.beforeEnter);
        $scope.$on('$ionicView.afterEnter', afterEnter);
        $scope.$on('$ionicView.beforeLeave', deregisterFns);
    };

    self.beforeEnter = function (ev, transData) {
        // this event was emitted, starting at intial ion-view, then bubbles up
        // only the first ion-view should do something with it, parent ion-views should ignore
        if (transData && !transData.viewNotified) {
            transData.viewNotified = true;

            if (!$rootScope.$$phase) $scope.$digest();
            viewTitle = isDefined($attrs.viewTitle) ? $attrs.viewTitle : $attrs.title;

            var navBarItems = {};
            for (var n in navElementHtml) {
                navBarItems[n] = generateNavBarItem(navElementHtml[n]);
            }

            navViewCtrl.beforeEnter(extend(transData, {
                title: viewTitle,
                showBack: !attrTrue('hideBackButton'),
                navBarItems: navBarItems,
                navBarDelegate: navBarDelegateHandle || null,
                showNavBar: !attrTrue('hideNavBar'),
                hasHeaderBar: !!hasViewHeaderBar
            }));

            // make sure any existing observers are cleaned up
            deregisterFns();
        }
    };


    function afterEnter() {
        // only listen for title updates after it has entered
        // but also deregister the observe before it leaves
        var viewTitleAttr = isDefined($attrs.viewTitle) && 'viewTitle' || isDefined($attrs.title) && 'title';
        if (viewTitleAttr) {
            titleUpdate($attrs[viewTitleAttr]);
            deregisters.push($attrs.$observe(viewTitleAttr, titleUpdate));
        }

        if (isDefined($attrs.hideBackButton)) {
            deregisters.push($scope.$watch($attrs.hideBackButton, function (val) {
                navViewCtrl.showBackButton(!val);
            }));
        }

        if (isDefined($attrs.hideNavBar)) {
            deregisters.push($scope.$watch($attrs.hideNavBar, function (val) {
                navViewCtrl.showBar(!val);
            }));
        }
    }


    function titleUpdate(newTitle) {
        if (isDefined(newTitle) && newTitle !== viewTitle) {
            viewTitle = newTitle;
            navViewCtrl.title(viewTitle);
        }
    }


    function deregisterFns() {
        // remove all existing $attrs.$observe's
        for (var x = 0; x < deregisters.length; x++) {
            deregisters[x]();
        }
        deregisters = [];
    }


    function generateNavBarItem(html) {
        if (html) {
            // every time a view enters we need to recreate its view buttons if they exist
            return $compile(html)($scope.$new());
        }
    }


    function attrTrue(key) {
        return !!$scope.$eval($attrs[key]);
    }


    self.navElement = function (type, html) {
        navElementHtml[type] = html;
    };

} ]);

    /*
    * We don't document the ionActionSheet directive, we instead document
    * the $ionicActionSheet service
    */
    IonicModule
.directive('ionActionSheet', ['$document', function ($document) {
    return {
        restrict: 'E',
        scope: true,
        replace: true,
        link: function ($scope, $element) {

            var keyUp = function (e) {
                if (e.which == 27) {
                    $scope.cancel();
                    $scope.$apply();
                }
            };

            var backdropClick = function (e) {
                if (e.target == $element[0]) {
                    $scope.cancel();
                    $scope.$apply();
                }
            };
            $scope.$on('$destroy', function () {
                $element.remove();
                $document.unbind('keyup', keyUp);
            });

            $document.bind('keyup', keyUp);
            $element.bind('click', backdropClick);
        },
        template: '<div class="action-sheet-backdrop">' +
                '<div class="action-sheet-wrapper">' +
                  '<div class="action-sheet" ng-class="{\'action-sheet-has-icons\': $actionSheetHasIcon}">' +
                    '<div class="action-sheet-group action-sheet-options">' +
                      '<div class="action-sheet-title" ng-if="titleText" ng-bind-html="titleText"></div>' +
                      '<button class="button action-sheet-option" ng-click="buttonClicked($index)" ng-class="b.className" ng-repeat="b in buttons" ng-bind-html="b.text"></button>' +
                      '<button class="button destructive action-sheet-destructive" ng-if="destructiveText" ng-click="destructiveButtonClicked()" ng-bind-html="destructiveText"></button>' +
                    '</div>' +
                    '<div class="action-sheet-group action-sheet-cancel" ng-if="cancelText">' +
                      '<button class="button" ng-click="cancel()" ng-bind-html="cancelText"></button>' +
                    '</div>' +
                  '</div>' +
                '</div>' +
              '</div>'
    };
} ]);


    /**
    * @ngdoc directive
    * @name ionCheckbox
    * @module ionic
    * @restrict E
    * @codepen hqcju
    * @description
    * The checkbox is no different than the HTML checkbox input, except it's styled differently.
    *
    * The checkbox behaves like any [AngularJS checkbox](http://docs.angularjs.org/api/ng/input/input[checkbox]).
    *
    * @usage
    * ```html
    * <ion-checkbox ng-model="isChecked">Checkbox Label</ion-checkbox>
    * ```
    */

    IonicModule
.directive('ionCheckbox', ['$ionicConfig', function ($ionicConfig) {
    return {
        restrict: 'E',
        replace: true,
        require: '?ngModel',
        transclude: true,
        template:
      '<label class="item item-checkbox">' +
        '<div class="checkbox checkbox-input-hidden disable-pointer-events">' +
          '<input type="checkbox">' +
          '<i class="checkbox-icon"></i>' +
        '</div>' +
        '<div class="item-content disable-pointer-events" ng-transclude></div>' +
      '</label>',
        compile: function (element, attr) {
            var input = element.find('input');
            forEach({
                'name': attr.name,
                'ng-value': attr.ngValue,
                'ng-model': attr.ngModel,
                'ng-checked': attr.ngChecked,
                'ng-disabled': attr.ngDisabled,
                'ng-true-value': attr.ngTrueValue,
                'ng-false-value': attr.ngFalseValue,
                'ng-change': attr.ngChange,
                'ng-required': attr.ngRequired,
                'required': attr.required
            }, function (value, name) {
                if (isDefined(value)) {
                    input.attr(name, value);
                }
            });
            var checkboxWrapper = element[0].querySelector('.checkbox');
            checkboxWrapper.classList.add('checkbox-' + $ionicConfig.form.checkbox());
        }
    };
} ]);


    /**
    * @ngdoc directive
    * @restrict A
    * @name collectionRepeat
    * @module ionic
    * @codepen 7ec1ec58f2489ab8f359fa1a0fe89c15
    * @description
    * `collection-repeat` allows an app to show huge lists of items much more performantly than
    * `ng-repeat`.
    *
    * It renders into the DOM only as many items as are currently visible.
    *
    * This means that on a phone screen that can fit eight items, only the eight items matching
    * the current scroll position will be rendered.
    *
    * **The Basics**:
    *
    * - The data given to collection-repeat must be an array.
    * - If the `item-height` and `item-width` attributes are not supplied, it will be assumed that
    *   every item in the list has the same dimensions as the first item.
    * - Don't use angular one-time binding (`::`) with collection-repeat. The scope of each item is
    *   assigned new data and re-digested as you scroll. Bindings need to update, and one-time bindings
    *   won't.
    *
    * **Performance Tips**:
    *
    * - The iOS webview has a performance bottleneck when switching out `<img src>` attributes.
    *   To increase performance of images on iOS, cache your images in advance and,
    *   if possible, lower the number of unique images. We're working on [a solution](https://github.com/driftyco/ionic/issues/3194).
    *
    * @usage
    * #### Basic Item List ([codepen](http://codepen.io/ionic/pen/0c2c35a34a8b18ad4d793fef0b081693))
    * ```html
    * <ion-content>
    *   <ion-item collection-repeat="item in items">
    *     {% raw %}{{item}}{% endraw %}
    *   </ion-item>
    * </ion-content>
    * ```
    *
    * #### Grid of Images ([codepen](http://codepen.io/ionic/pen/5515d4efd9d66f780e96787387f41664))
    * ```html
    * <ion-content>
    *   <img collection-repeat="photo in photos"
    *     item-width="33%"
    *     item-height="200px"
    *     ng-src="{% raw %}{{photo.url}}{% endraw %}">
    * </ion-content>
    * ```
    *
    * #### Horizontal Scroller, Dynamic Item Width ([codepen](http://codepen.io/ionic/pen/67cc56b349124a349acb57a0740e030e))
    * ```html
    * <ion-content>
    *   <h2>Available Kittens:</h2>
    *   <ion-scroll direction="x" class="available-scroller">
    *     <div class="photo" collection-repeat="photo in main.photos"
    *        item-height="250" item-width="photo.width + 30">
    *        <img ng-src="{% raw %}{{photo.src}}{% endraw %}">
    *     </div>
    *   </ion-scroll>
    * </ion-content>
    * ```
    *
    * @param {expression} collection-repeat The expression indicating how to enumerate a collection,
    *   of the format  `variable in expression` – where variable is the user defined loop variable
    *   and `expression` is a scope expression giving the collection to enumerate.
    *   For example: `album in artist.albums` or `album in artist.albums | orderBy:'name'`.
    * @param {expression=} item-width The width of the repeated element. The expression must return
    *   a number (pixels) or a percentage. Defaults to the width of the first item in the list.
    *   (previously named collection-item-width)
    * @param {expression=} item-height The height of the repeated element. The expression must return
    *   a number (pixels) or a percentage. Defaults to the height of the first item in the list.
    *   (previously named collection-item-height)
    * @param {number=} item-render-buffer The number of items to load before and after the visible
    *   items in the list. Default 3. Tip: set this higher if you have lots of images to preload, but
    *   don't set it too high or you'll see performance loss.
    * @param {boolean=} force-refresh-images Force images to refresh as you scroll. This fixes a problem
    *   where, when an element is interchanged as scrolling, its image will still have the old src
    *   while the new src loads. Setting this to true comes with a small performance loss.
    */

    IonicModule
.directive('collectionRepeat', CollectionRepeatDirective)
.factory('$ionicCollectionManager', RepeatManagerFactory);

    var ONE_PX_TRANSPARENT_IMG_SRC = '';
    var WIDTH_HEIGHT_REGEX = /height:.*?px;\s*width:.*?px/;
    var DEFAULT_RENDER_BUFFER = 3;

    CollectionRepeatDirective.$inject = ['$ionicCollectionManager', '$parse', '$window', '$$rAF', '$rootScope', '$timeout'];
    function CollectionRepeatDirective($ionicCollectionManager, $parse, $window, $$rAF, $rootScope, $timeout) {
        return {
            restrict: 'A',
            priority: 1000,
            transclude: 'element',
            $$tlb: true,
            require: '^^$ionicScroll',
            link: postLink
        };

        function postLink(scope, element, attr, scrollCtrl, transclude) {
            var scrollView = scrollCtrl.scrollView;
            var node = element[0];
            var containerNode = angular.element('<div class="collection-repeat-container">')[0];
            node.parentNode.replaceChild(containerNode, node);

            if (scrollView.options.scrollingX && scrollView.options.scrollingY) {
                throw new Error("collection-repeat expected a parent x or y scrollView, not " +
                      "an xy scrollView.");
            }

            var repeatExpr = attr.collectionRepeat;
            var match = repeatExpr.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
            if (!match) {
                throw new Error("collection-repeat expected expression in form of '_item_ in " +
                      "_collection_[ track by _id_]' but got '" + attr.collectionRepeat + "'.");
            }
            var keyExpr = match[1];
            var listExpr = match[2];
            var listGetter = $parse(listExpr);
            var heightData = {};
            var widthData = {};
            var computedStyleDimensions = {};
            var data = [];
            var repeatManager;

            // attr.collectionBufferSize is deprecated
            var renderBufferExpr = attr.itemRenderBuffer || attr.collectionBufferSize;
            var renderBuffer = angular.isDefined(renderBufferExpr) ?
      parseInt(renderBufferExpr) :
      DEFAULT_RENDER_BUFFER;

            // attr.collectionItemHeight is deprecated
            var heightExpr = attr.itemHeight || attr.collectionItemHeight;
            // attr.collectionItemWidth is deprecated
            var widthExpr = attr.itemWidth || attr.collectionItemWidth;

            var afterItemsContainer = initAfterItemsContainer();

            var changeValidator = makeChangeValidator();
            initDimensions();

            // Dimensions are refreshed on resize or data change.
            scrollCtrl.$element.on('scroll-resize', refreshDimensions);

            angular.element($window).on('resize', onResize);
            var unlistenToExposeAside = $rootScope.$on('$ionicExposeAside', ionic.animationFrameThrottle(function () {
                scrollCtrl.scrollView.resize();
                onResize();
            }));
            $timeout(refreshDimensions, 0, false);

            function onResize() {
                if (changeValidator.resizeRequiresRefresh(scrollView.__clientWidth, scrollView.__clientHeight)) {
                    refreshDimensions();
                }
            }

            scope.$watchCollection(listGetter, function (newValue) {
                data = newValue || (newValue = []);
                if (!angular.isArray(newValue)) {
                    throw new Error("collection-repeat expected an array for '" + listExpr + "', " +
          "but got a " + typeof value);
                }
                // Wait for this digest to end before refreshing everything.
                scope.$$postDigest(function () {
                    getRepeatManager().setData(data);
                    if (changeValidator.dataChangeRequiresRefresh(data)) refreshDimensions();
                });
            });

            scope.$on('$destroy', function () {
                angular.element($window).off('resize', onResize);
                unlistenToExposeAside();
                scrollCtrl.$element && scrollCtrl.$element.off('scroll-resize', refreshDimensions);

                computedStyleNode && computedStyleNode.parentNode &&
        computedStyleNode.parentNode.removeChild(computedStyleNode);
                computedStyleScope && computedStyleScope.$destroy();
                computedStyleScope = computedStyleNode = null;

                repeatManager && repeatManager.destroy();
                repeatManager = null;
            });

            function makeChangeValidator() {
                var self;
                return (self = {
                    dataLength: 0,
                    width: 0,
                    height: 0,
                    // A resize triggers a refresh only if we have data, the scrollView has size,
                    // and the size has changed.
                    resizeRequiresRefresh: function (newWidth, newHeight) {
                        var requiresRefresh = self.dataLength && newWidth && newHeight &&
            (newWidth !== self.width || newHeight !== self.height);

                        self.width = newWidth;
                        self.height = newHeight;

                        return !!requiresRefresh;
                    },
                    // A change in data only triggers a refresh if the data has length, or if the data's
                    // length is less than before.
                    dataChangeRequiresRefresh: function (newData) {
                        var requiresRefresh = newData.length > 0 || newData.length < self.dataLength;

                        self.dataLength = newData.length;

                        return !!requiresRefresh;
                    }
                });
            }

            function getRepeatManager() {
                return repeatManager || (repeatManager = new $ionicCollectionManager({
                    afterItemsNode: afterItemsContainer[0],
                    containerNode: containerNode,
                    heightData: heightData,
                    widthData: widthData,
                    forceRefreshImages: !!(isDefined(attr.forceRefreshImages) && attr.forceRefreshImages !== 'false'),
                    keyExpression: keyExpr,
                    renderBuffer: renderBuffer,
                    scope: scope,
                    scrollView: scrollCtrl.scrollView,
                    transclude: transclude
                }));
            }

            function initAfterItemsContainer() {
                var container = angular.element(
        scrollView.__content.querySelector('.collection-repeat-after-container')
      );
                // Put everything in the view after the repeater into a container.
                if (!container.length) {
                    var elementIsAfterRepeater = false;
                    var afterNodes = [].filter.call(scrollView.__content.childNodes, function (node) {
                        if (ionic.DomUtil.contains(node, containerNode)) {
                            elementIsAfterRepeater = true;
                            return false;
                        }
                        return elementIsAfterRepeater;
                    });
                    container = angular.element('<span class="collection-repeat-after-container">');
                    if (scrollView.options.scrollingX) {
                        container.addClass('horizontal');
                    }
                    container.append(afterNodes);
                    scrollView.__content.appendChild(container[0]);
                }
                return container;
            }

            function initDimensions() {
                //Height and width have four 'modes':
                //1) Computed Mode
                //  - Nothing is supplied, so we getComputedStyle() on one element in the list and use
                //    that width and height value for the width and height of every item. This is re-computed
                //    every resize.
                //2) Constant Mode, Static Integer
                //  - The user provides a constant number for width or height, in pixels. We parse it,
                //    store it on the `value` field, and it never changes
                //3) Constant Mode, Percent
                //  - The user provides a percent string for width or height. The getter for percent is
                //    stored on the `getValue()` field, and is re-evaluated once every resize. The result
                //    is stored on the `value` field.
                //4) Dynamic Mode
                //  - The user provides a dynamic expression for the width or height.  This is re-evaluated
                //    for every item, stored on the `.getValue()` field.
                if (heightExpr) {
                    parseDimensionAttr(heightExpr, heightData);
                } else {
                    heightData.computed = true;
                }
                if (widthExpr) {
                    parseDimensionAttr(widthExpr, widthData);
                } else {
                    widthData.computed = true;
                }
            }

            function refreshDimensions() {
                var hasData = data.length > 0;

                if (hasData && (heightData.computed || widthData.computed)) {
                    computeStyleDimensions();
                }

                if (hasData && heightData.computed) {
                    heightData.value = computedStyleDimensions.height;
                    if (!heightData.value) {
                        throw new Error('collection-repeat tried to compute the height of repeated elements "' +
            repeatExpr + '", but was unable to. Please provide the "item-height" attribute. ' +
            'http://ionicframework.com/docs/api/directive/collectionRepeat/');
                    }
                } else if (!heightData.dynamic && heightData.getValue) {
                    // If it's a constant with a getter (eg percent), we just refresh .value after resize
                    heightData.value = heightData.getValue();
                }

                if (hasData && widthData.computed) {
                    widthData.value = computedStyleDimensions.width;
                    if (!widthData.value) {
                        throw new Error('collection-repeat tried to compute the width of repeated elements "' +
            repeatExpr + '", but was unable to. Please provide the "item-width" attribute. ' +
            'http://ionicframework.com/docs/api/directive/collectionRepeat/');
                    }
                } else if (!widthData.dynamic && widthData.getValue) {
                    // If it's a constant with a getter (eg percent), we just refresh .value after resize
                    widthData.value = widthData.getValue();
                }
                // Dynamic dimensions aren't updated on resize. Since they're already dynamic anyway,
                // .getValue() will be used.

                getRepeatManager().refreshLayout();
            }

            function parseDimensionAttr(attrValue, dimensionData) {
                if (!attrValue) return;

                var parsedValue;
                // Try to just parse the plain attr value
                try {
                    parsedValue = $parse(attrValue);
                } catch (e) {
                    // If the parse fails and the value has `px` or `%` in it, surround the attr in
                    // quotes, to attempt to let the user provide a simple `attr="100%"` or `attr="100px"`
                    if (attrValue.trim().match(/\d+(px|%)$/)) {
                        attrValue = '"' + attrValue + '"';
                    }
                    parsedValue = $parse(attrValue);
                }

                var constantAttrValue = attrValue.replace(/(\'|\"|px|%)/g, '').trim();
                var isConstant = constantAttrValue.length && !/([a-zA-Z]|\$|:|\?)/.test(constantAttrValue);
                dimensionData.attrValue = attrValue;

                // If it's a constant, it's either a percent or just a constant pixel number.
                if (isConstant) {
                    // For percents, store the percent getter on .getValue()
                    if (attrValue.indexOf('%') > -1) {
                        var decimalValue = parseFloat(parsedValue()) / 100;
                        dimensionData.getValue = dimensionData === heightData ?
            function () { return Math.floor(decimalValue * scrollView.__clientHeight); } :
            function () { return Math.floor(decimalValue * scrollView.__clientWidth); };
                    } else {
                        // For static constants, just store the static constant.
                        dimensionData.value = parseInt(parsedValue());
                    }

                } else {
                    dimensionData.dynamic = true;
                    dimensionData.getValue = dimensionData === heightData ?
          function heightGetter(scope, locals) {
              var result = parsedValue(scope, locals);
              if (result.charAt && result.charAt(result.length - 1) === '%') {
                  return Math.floor(parseFloat(result) / 100 * scrollView.__clientHeight);
              }
              return parseInt(result);
          } :
          function widthGetter(scope, locals) {
              var result = parsedValue(scope, locals);
              if (result.charAt && result.charAt(result.length - 1) === '%') {
                  return Math.floor(parseFloat(result) / 100 * scrollView.__clientWidth);
              }
              return parseInt(result);
          };
                }
            }

            var computedStyleNode;
            var computedStyleScope;
            function computeStyleDimensions() {
                if (!computedStyleNode) {
                    transclude(computedStyleScope = scope.$new(), function (clone) {
                        clone[0].removeAttribute('collection-repeat'); // remove absolute position styling
                        computedStyleNode = clone[0];
                    });
                }

                computedStyleScope[keyExpr] = (listGetter(scope) || [])[0];
                if (!$rootScope.$$phase) computedStyleScope.$digest();
                containerNode.appendChild(computedStyleNode);

                var style = $window.getComputedStyle(computedStyleNode);
                computedStyleDimensions.width = parseInt(style.width);
                computedStyleDimensions.height = parseInt(style.height);

                containerNode.removeChild(computedStyleNode);
            }

        }

    }

    RepeatManagerFactory.$inject = ['$rootScope', '$window', '$$rAF'];
    function RepeatManagerFactory($rootScope, $window, $$rAF) {
        var EMPTY_DIMENSION = { primaryPos: 0, secondaryPos: 0, primarySize: 0, secondarySize: 0, rowPrimarySize: 0 };

        return function RepeatController(options) {
            var afterItemsNode = options.afterItemsNode;
            var containerNode = options.containerNode;
            var forceRefreshImages = options.forceRefreshImages;
            var heightData = options.heightData;
            var widthData = options.widthData;
            var keyExpression = options.keyExpression;
            var renderBuffer = options.renderBuffer;
            var scope = options.scope;
            var scrollView = options.scrollView;
            var transclude = options.transclude;

            var data = [];

            var getterLocals = {};
            var heightFn = heightData.getValue || function () { return heightData.value; };
            var heightGetter = function (index, value) {
                getterLocals[keyExpression] = value;
                getterLocals.$index = index;
                return heightFn(scope, getterLocals);
            };

            var widthFn = widthData.getValue || function () { return widthData.value; };
            var widthGetter = function (index, value) {
                getterLocals[keyExpression] = value;
                getterLocals.$index = index;
                return widthFn(scope, getterLocals);
            };

            var isVertical = !!scrollView.options.scrollingY;

            // We say it's a grid view if we're either dynamic or not 100% width
            var isGridView = isVertical ?
      (widthData.dynamic || widthData.value !== scrollView.__clientWidth) :
      (heightData.dynamic || heightData.value !== scrollView.__clientHeight);

            var isStaticView = !heightData.dynamic && !widthData.dynamic;

            var PRIMARY = 'PRIMARY';
            var SECONDARY = 'SECONDARY';
            var TRANSLATE_TEMPLATE_STR = isVertical ?
      'translate3d(SECONDARYpx,PRIMARYpx,0)' :
      'translate3d(PRIMARYpx,SECONDARYpx,0)';
            var WIDTH_HEIGHT_TEMPLATE_STR = isVertical ?
      'height: PRIMARYpx; width: SECONDARYpx;' :
      'height: SECONDARYpx; width: PRIMARYpx;';

            var estimatedHeight;
            var estimatedWidth;

            var repeaterBeforeSize = 0;
            var repeaterAfterSize = 0;

            var renderStartIndex = -1;
            var renderEndIndex = -1;
            var renderAfterBoundary = -1;
            var renderBeforeBoundary = -1;

            var itemsPool = [];
            var itemsLeaving = [];
            var itemsEntering = [];
            var itemsShownMap = {};
            var nextItemId = 0;

            var scrollViewSetDimensions = isVertical ?
      function () { scrollView.setDimensions(null, null, null, view.getContentSize(), true); } :
      function () { scrollView.setDimensions(null, null, view.getContentSize(), null, true); };

            // view is a mix of list/grid methods + static/dynamic methods.
            // See bottom for implementations. Available methods:
            //
            // getEstimatedPrimaryPos(i), getEstimatedSecondaryPos(i), getEstimatedIndex(scrollTop),
            // calculateDimensions(toIndex), getDimensions(index),
            // updateRenderRange(scrollTop, scrollValueEnd), onRefreshLayout(), onRefreshData()
            var view = isVertical ? new VerticalViewType() : new HorizontalViewType();
            (isGridView ? GridViewType : ListViewType).call(view);
            (isStaticView ? StaticViewType : DynamicViewType).call(view);

            var contentSizeStr = isVertical ? 'getContentHeight' : 'getContentWidth';
            var originalGetContentSize = scrollView.options[contentSizeStr];
            scrollView.options[contentSizeStr] = angular.bind(view, view.getContentSize);

            scrollView.__$callback = scrollView.__callback;
            scrollView.__callback = function (transformLeft, transformTop, zoom, wasResize) {
                var scrollValue = view.getScrollValue();
                if (renderStartIndex === -1 ||
          scrollValue + view.scrollPrimarySize > renderAfterBoundary ||
          scrollValue < renderBeforeBoundary) {
                    render();
                }
                scrollView.__$callback(transformLeft, transformTop, zoom, wasResize);
            };

            var isLayoutReady = false;
            var isDataReady = false;
            this.refreshLayout = function () {
                if (data.length) {
                    estimatedHeight = heightGetter(0, data[0]);
                    estimatedWidth = widthGetter(0, data[0]);
                } else {
                    // If we don't have any data in our array, just guess.
                    estimatedHeight = 100;
                    estimatedWidth = 100;
                }

                // Get the size of every element AFTER the repeater. We have to get the margin before and
                // after the first/last element to fix a browser bug with getComputedStyle() not counting
                // the first/last child's margins into height.
                var style = getComputedStyle(afterItemsNode) || {};
                var firstStyle = afterItemsNode.firstElementChild && getComputedStyle(afterItemsNode.firstElementChild) || {};
                var lastStyle = afterItemsNode.lastElementChild && getComputedStyle(afterItemsNode.lastElementChild) || {};
                repeaterAfterSize = (parseInt(style[isVertical ? 'height' : 'width']) || 0) +
        (firstStyle && parseInt(firstStyle[isVertical ? 'marginTop' : 'marginLeft']) || 0) +
        (lastStyle && parseInt(lastStyle[isVertical ? 'marginBottom' : 'marginRight']) || 0);

                // Get the offsetTop of the repeater.
                repeaterBeforeSize = 0;
                var current = containerNode;
                do {
                    repeaterBeforeSize += current[isVertical ? 'offsetTop' : 'offsetLeft'];
                } while (ionic.DomUtil.contains(scrollView.__content, current = current.offsetParent));

                var containerPrevNode = containerNode.previousElementSibling;
                var beforeStyle = containerPrevNode ? $window.getComputedStyle(containerPrevNode) : {};
                var beforeMargin = parseInt(beforeStyle[isVertical ? 'marginBottom' : 'marginRight'] || 0);

                // Because we position the collection container with position: relative, it doesn't take
                // into account where to position itself relative to the previous element's marginBottom.
                // To compensate, we translate the container up by the previous element's margin.
                containerNode.style[ionic.CSS.TRANSFORM] = TRANSLATE_TEMPLATE_STR
        .replace(PRIMARY, -beforeMargin)
        .replace(SECONDARY, 0);
                repeaterBeforeSize -= beforeMargin;

                if (!scrollView.__clientHeight || !scrollView.__clientWidth) {
                    scrollView.__clientWidth = scrollView.__container.clientWidth;
                    scrollView.__clientHeight = scrollView.__container.clientHeight;
                }

                (view.onRefreshLayout || angular.noop)();
                view.refreshDirection();
                scrollViewSetDimensions();

                // Create the pool of items for reuse, setting the size to (estimatedItemsOnScreen) * 2,
                // plus the size of the renderBuffer.
                if (!isLayoutReady) {
                    var poolSize = Math.max(20, renderBuffer * 3);
                    for (var i = 0; i < poolSize; i++) {
                        itemsPool.push(new RepeatItem());
                    }
                }

                isLayoutReady = true;
                if (isLayoutReady && isDataReady) {
                    // If the resize or latest data change caused the scrollValue to
                    // now be out of bounds, resize the scrollView.
                    if (scrollView.__scrollLeft > scrollView.__maxScrollLeft ||
            scrollView.__scrollTop > scrollView.__maxScrollTop) {
                        scrollView.resize();
                    }
                    forceRerender(true);
                }
            };

            this.setData = function (newData) {
                data = newData;
                (view.onRefreshData || angular.noop)();
                isDataReady = true;
            };

            this.destroy = function () {
                render.destroyed = true;

                itemsPool.forEach(function (item) {
                    item.scope.$destroy();
                    item.scope = item.element = item.node = item.images = null;
                });
                itemsPool.length = itemsEntering.length = itemsLeaving.length = 0;
                itemsShownMap = {};

                //Restore the scrollView's normal behavior and resize it to normal size.
                scrollView.options[contentSizeStr] = originalGetContentSize;
                scrollView.__callback = scrollView.__$callback;
                scrollView.resize();

                (view.onDestroy || angular.noop)();
            };

            function forceRerender() {
                return render(true);
            }
            function render(forceRerender) {
                if (render.destroyed) return;
                var i;
                var ii;
                var item;
                var dim;
                var scope;
                var scrollValue = view.getScrollValue();
                var scrollValueEnd = scrollValue + view.scrollPrimarySize;

                view.updateRenderRange(scrollValue, scrollValueEnd);

                renderStartIndex = Math.max(0, renderStartIndex - renderBuffer);
                renderEndIndex = Math.min(data.length - 1, renderEndIndex + renderBuffer);

                for (i in itemsShownMap) {
                    if (i < renderStartIndex || i > renderEndIndex) {
                        item = itemsShownMap[i];
                        delete itemsShownMap[i];
                        itemsLeaving.push(item);
                        item.isShown = false;
                    }
                }

                // Render indicies that aren't shown yet
                //
                // NOTE(ajoslin): this may sound crazy, but calling any other functions during this render
                // loop will often push the render time over the edge from less than one frame to over
                // one frame, causing visible jank.
                // DON'T call any other functions inside this loop unless it's vital.
                for (i = renderStartIndex; i <= renderEndIndex; i++) {
                    // We only go forward with render if the index is in data, the item isn't already shown,
                    // or forceRerender is on.
                    if (i >= data.length || (itemsShownMap[i] && !forceRerender)) continue;

                    item = itemsShownMap[i] || (itemsShownMap[i] = itemsLeaving.length ? itemsLeaving.pop() :
                                    itemsPool.length ? itemsPool.shift() :
                                    new RepeatItem());
                    itemsEntering.push(item);
                    item.isShown = true;

                    scope = item.scope;
                    scope.$index = i;
                    scope[keyExpression] = data[i];
                    scope.$first = (i === 0);
                    scope.$last = (i === (data.length - 1));
                    scope.$middle = !(scope.$first || scope.$last);
                    scope.$odd = !(scope.$even = (i & 1) === 0);

                    if (scope.$$disconnected) ionic.Utils.reconnectScope(item.scope);

                    dim = view.getDimensions(i);
                    if (item.secondaryPos !== dim.secondaryPos || item.primaryPos !== dim.primaryPos) {
                        item.node.style[ionic.CSS.TRANSFORM] = TRANSLATE_TEMPLATE_STR
            .replace(PRIMARY, (item.primaryPos = dim.primaryPos))
            .replace(SECONDARY, (item.secondaryPos = dim.secondaryPos));
                    }
                    if (item.secondarySize !== dim.secondarySize || item.primarySize !== dim.primarySize) {
                        item.node.style.cssText = item.node.style.cssText
            .replace(WIDTH_HEIGHT_REGEX, WIDTH_HEIGHT_TEMPLATE_STR
                        //TODO fix item.primarySize + 1 hack
              .replace(PRIMARY, (item.primarySize = dim.primarySize) + 1)
              .replace(SECONDARY, (item.secondarySize = dim.secondarySize))
            );
                    }

                }

                // If we reach the end of the list, render the afterItemsNode - this contains all the
                // elements the developer placed after the collection-repeat
                if (renderEndIndex === data.length - 1) {
                    dim = view.getDimensions(data.length - 1) || EMPTY_DIMENSION;
                    afterItemsNode.style[ionic.CSS.TRANSFORM] = TRANSLATE_TEMPLATE_STR
          .replace(PRIMARY, dim.primaryPos + dim.primarySize)
          .replace(SECONDARY, 0);
                }

                while (itemsLeaving.length) {
                    item = itemsLeaving.pop();
                    item.scope.$broadcast('$collectionRepeatLeave');
                    ionic.Utils.disconnectScope(item.scope);
                    itemsPool.push(item);
                    item.node.style[ionic.CSS.TRANSFORM] = 'translate3d(-9999px,-9999px,0)';
                    item.primaryPos = item.secondaryPos = null;
                }

                if (forceRefreshImages) {
                    for (i = 0, ii = itemsEntering.length; i < ii && (item = itemsEntering[i]); i++) {
                        if (!item.images) continue;
                        for (var j = 0, jj = item.images.length, img; j < jj && (img = item.images[j]); j++) {
                            var src = img.src;
                            img.src = ONE_PX_TRANSPARENT_IMG_SRC;
                            img.src = src;
                        }
                    }
                }
                if (forceRerender) {
                    var rootScopePhase = $rootScope.$$phase;
                    while (itemsEntering.length) {
                        item = itemsEntering.pop();
                        if (!rootScopePhase) item.scope.$digest();
                    }
                } else {
                    digestEnteringItems();
                }
            }

            function digestEnteringItems() {
                var item;
                if (digestEnteringItems.running) return;
                digestEnteringItems.running = true;

                $$rAF(function process() {
                    var rootScopePhase = $rootScope.$$phase;
                    while (itemsEntering.length) {
                        item = itemsEntering.pop();
                        if (item.isShown) {
                            if (!rootScopePhase) item.scope.$digest();
                        }
                    }
                    digestEnteringItems.running = false;
                });
            }

            function RepeatItem() {
                var self = this;
                this.scope = scope.$new();
                this.id = 'item' + (nextItemId++);
                transclude(this.scope, function (clone) {
                    self.element = clone;
                    self.element.data('$$collectionRepeatItem', self);
                    // TODO destroy
                    self.node = clone[0];
                    // Batch style setting to lower repaints
                    self.node.style[ionic.CSS.TRANSFORM] = 'translate3d(-9999px,-9999px,0)';
                    self.node.style.cssText += ' height: 0px; width: 0px;';
                    ionic.Utils.disconnectScope(self.scope);
                    containerNode.appendChild(self.node);
                    self.images = clone[0].getElementsByTagName('img');
                });
            }

            function VerticalViewType() {
                this.getItemPrimarySize = heightGetter;
                this.getItemSecondarySize = widthGetter;

                this.getScrollValue = function () {
                    return Math.max(0, Math.min(scrollView.__scrollTop - repeaterBeforeSize,
          scrollView.__maxScrollTop - repeaterBeforeSize - repeaterAfterSize));
                };

                this.refreshDirection = function () {
                    this.scrollPrimarySize = scrollView.__clientHeight;
                    this.scrollSecondarySize = scrollView.__clientWidth;

                    this.estimatedPrimarySize = estimatedHeight;
                    this.estimatedSecondarySize = estimatedWidth;
                    this.estimatedItemsAcross = isGridView &&
          Math.floor(scrollView.__clientWidth / estimatedWidth) ||
          1;
                };
            }
            function HorizontalViewType() {
                this.getItemPrimarySize = widthGetter;
                this.getItemSecondarySize = heightGetter;

                this.getScrollValue = function () {
                    return Math.max(0, Math.min(scrollView.__scrollLeft - repeaterBeforeSize,
          scrollView.__maxScrollLeft - repeaterBeforeSize - repeaterAfterSize));
                };

                this.refreshDirection = function () {
                    this.scrollPrimarySize = scrollView.__clientWidth;
                    this.scrollSecondarySize = scrollView.__clientHeight;

                    this.estimatedPrimarySize = estimatedWidth;
                    this.estimatedSecondarySize = estimatedHeight;
                    this.estimatedItemsAcross = isGridView &&
          Math.floor(scrollView.__clientHeight / estimatedHeight) ||
          1;
                };
            }

            function GridViewType() {
                this.getEstimatedSecondaryPos = function (index) {
                    return (index % this.estimatedItemsAcross) * this.estimatedSecondarySize;
                };
                this.getEstimatedPrimaryPos = function (index) {
                    return Math.floor(index / this.estimatedItemsAcross) * this.estimatedPrimarySize;
                };
                this.getEstimatedIndex = function (scrollValue) {
                    return Math.floor(scrollValue / this.estimatedPrimarySize) *
          this.estimatedItemsAcross;
                };
            }

            function ListViewType() {
                this.getEstimatedSecondaryPos = function () {
                    return 0;
                };
                this.getEstimatedPrimaryPos = function (index) {
                    return index * this.estimatedPrimarySize;
                };
                this.getEstimatedIndex = function (scrollValue) {
                    return Math.floor((scrollValue) / this.estimatedPrimarySize);
                };
            }

            function StaticViewType() {
                this.getContentSize = function () {
                    return this.getEstimatedPrimaryPos(data.length - 1) + this.estimatedPrimarySize +
          repeaterBeforeSize + repeaterAfterSize;
                };
                // static view always returns the same object for getDimensions, to avoid memory allocation
                // while scrolling. This could be dangerous if this was a public function, but it's not.
                // Only we use it.
                var dim = {};
                this.getDimensions = function (index) {
                    dim.primaryPos = this.getEstimatedPrimaryPos(index);
                    dim.secondaryPos = this.getEstimatedSecondaryPos(index);
                    dim.primarySize = this.estimatedPrimarySize;
                    dim.secondarySize = this.estimatedSecondarySize;
                    return dim;
                };
                this.updateRenderRange = function (scrollValue, scrollValueEnd) {
                    renderStartIndex = Math.max(0, this.getEstimatedIndex(scrollValue));

                    // Make sure the renderEndIndex takes into account all the items on the row
                    renderEndIndex = Math.min(data.length - 1,
          this.getEstimatedIndex(scrollValueEnd) + this.estimatedItemsAcross - 1);

                    renderBeforeBoundary = Math.max(0,
          this.getEstimatedPrimaryPos(renderStartIndex));
                    renderAfterBoundary = this.getEstimatedPrimaryPos(renderEndIndex) +
          this.estimatedPrimarySize;
                };
            }

            function DynamicViewType() {
                var self = this;
                var debouncedScrollViewSetDimensions = ionic.debounce(scrollViewSetDimensions, 25, true);
                var calculateDimensions = isGridView ? calculateDimensionsGrid : calculateDimensionsList;
                var dimensionsIndex;
                var dimensions = [];


                // Get the dimensions at index. {width, height, left, top}.
                // We start with no dimensions calculated, then any time dimensions are asked for at an
                // index we calculate dimensions up to there.
                function calculateDimensionsList(toIndex) {
                    var i, prevDimension, dim;
                    for (i = Math.max(0, dimensionsIndex); i <= toIndex && (dim = dimensions[i]); i++) {
                        prevDimension = dimensions[i - 1] || EMPTY_DIMENSION;
                        dim.primarySize = self.getItemPrimarySize(i, data[i]);
                        dim.secondarySize = self.scrollSecondarySize;
                        dim.primaryPos = prevDimension.primaryPos + prevDimension.primarySize;
                        dim.secondaryPos = 0;
                    }
                }
                function calculateDimensionsGrid(toIndex) {
                    var i, prevDimension, dim;
                    for (i = Math.max(dimensionsIndex, 0); i <= toIndex && (dim = dimensions[i]); i++) {
                        prevDimension = dimensions[i - 1] || EMPTY_DIMENSION;
                        dim.secondarySize = Math.min(
            self.getItemSecondarySize(i, data[i]),
            self.scrollSecondarySize
          );
                        dim.secondaryPos = prevDimension.secondaryPos + prevDimension.secondarySize;

                        if (i === 0 || dim.secondaryPos + dim.secondarySize > self.scrollSecondarySize) {
                            dim.secondaryPos = 0;
                            dim.primarySize = self.getItemPrimarySize(i, data[i]);
                            dim.primaryPos = prevDimension.primaryPos + prevDimension.rowPrimarySize;

                            dim.rowStartIndex = i;
                            dim.rowPrimarySize = dim.primarySize;
                        } else {
                            dim.primarySize = self.getItemPrimarySize(i, data[i]);
                            dim.primaryPos = prevDimension.primaryPos;
                            dim.rowStartIndex = prevDimension.rowStartIndex;

                            dimensions[dim.rowStartIndex].rowPrimarySize = dim.rowPrimarySize = Math.max(
              dimensions[dim.rowStartIndex].rowPrimarySize,
              dim.primarySize
            );
                            dim.rowPrimarySize = Math.max(dim.primarySize, dim.rowPrimarySize);
                        }
                    }
                }

                this.getContentSize = function () {
                    var dim = dimensions[dimensionsIndex] || EMPTY_DIMENSION;
                    return ((dim.primaryPos + dim.primarySize) || 0) +
          this.getEstimatedPrimaryPos(data.length - dimensionsIndex - 1) +
          repeaterBeforeSize + repeaterAfterSize;
                };
                this.onDestroy = function () {
                    dimensions.length = 0;
                };

                this.onRefreshData = function () {
                    var i;
                    var ii;
                    // Make sure dimensions has as many items as data.length.
                    // This is to be sure we don't have to allocate objects while scrolling.
                    for (i = dimensions.length, ii = data.length; i < ii; i++) {
                        dimensions.push({});
                    }
                    dimensionsIndex = -1;
                };
                this.onRefreshLayout = function () {
                    dimensionsIndex = -1;
                };
                this.getDimensions = function (index) {
                    index = Math.min(index, data.length - 1);

                    if (dimensionsIndex < index) {
                        // Once we start asking for dimensions near the end of the list, go ahead and calculate
                        // everything. This is to make sure when the user gets to the end of the list, the
                        // scroll height of the list is 100% accurate (not estimated anymore).
                        if (index > data.length * 0.9) {
                            calculateDimensions(data.length - 1);
                            dimensionsIndex = data.length - 1;
                            scrollViewSetDimensions();
                        } else {
                            calculateDimensions(index);
                            dimensionsIndex = index;
                            debouncedScrollViewSetDimensions();
                        }

                    }
                    return dimensions[index];
                };

                var oldRenderStartIndex = -1;
                var oldScrollValue = -1;
                this.updateRenderRange = function (scrollValue, scrollValueEnd) {
                    var i;
                    var len;
                    var dim;

                    // Calculate more dimensions than we estimate we'll need, to be sure.
                    this.getDimensions(this.getEstimatedIndex(scrollValueEnd) * 2);

                    // -- Calculate renderStartIndex
                    // base case: start at 0
                    if (oldRenderStartIndex === -1 || scrollValue === 0) {
                        i = 0;
                        // scrolling down
                    } else if (scrollValue >= oldScrollValue) {
                        for (i = oldRenderStartIndex, len = data.length; i < len; i++) {
                            if ((dim = this.getDimensions(i)) && dim.primaryPos + dim.rowPrimarySize >= scrollValue) {
                                break;
                            }
                        }
                        // scrolling up
                    } else {
                        for (i = oldRenderStartIndex; i >= 0; i--) {
                            if ((dim = this.getDimensions(i)) && dim.primaryPos <= scrollValue) {
                                // when grid view, make sure the render starts at the beginning of a row.
                                i = isGridView ? dim.rowStartIndex : i;
                                break;
                            }
                        }
                    }

                    renderStartIndex = Math.min(Math.max(0, i), data.length - 1);
                    renderBeforeBoundary = renderStartIndex !== -1 ? this.getDimensions(renderStartIndex).primaryPos : -1;

                    // -- Calculate renderEndIndex
                    var lastRowDim;
                    for (i = renderStartIndex + 1, len = data.length; i < len; i++) {
                        if ((dim = this.getDimensions(i)) && dim.primaryPos + dim.rowPrimarySize > scrollValueEnd) {

                            // Go all the way to the end of the row if we're in a grid
                            if (isGridView) {
                                lastRowDim = dim;
                                while (i < len - 1 &&
                    (dim = this.getDimensions(i + 1)).primaryPos === lastRowDim.primaryPos) {
                                    i++;
                                }
                            }
                            break;
                        }
                    }

                    renderEndIndex = Math.min(i, data.length - 1);
                    renderAfterBoundary = renderEndIndex !== -1 ?
          ((dim = this.getDimensions(renderEndIndex)).primaryPos + (dim.rowPrimarySize || dim.primarySize)) :
          -1;

                    oldScrollValue = scrollValue;
                    oldRenderStartIndex = renderStartIndex;
                };
            }


        };

    }

    /**
    * @ngdoc directive
    * @name ionContent
    * @module ionic
    * @delegate ionic.service:$ionicScrollDelegate
    * @restrict E
    *
    * @description
    * The ionContent directive provides an easy to use content area that can be configured
    * to use Ionic's custom Scroll View, or the built in overflow scrolling of the browser.
    *
    * While we recommend using the custom Scroll features in Ionic in most cases, sometimes
    * (for performance reasons) only the browser's native overflow scrolling will suffice,
    * and so we've made it easy to toggle between the Ionic scroll implementation and
    * overflow scrolling.
    *
    * You can implement pull-to-refresh with the {@link ionic.directive:ionRefresher}
    * directive, and infinite scrolling with the {@link ionic.directive:ionInfiniteScroll}
    * directive.
    *
    * If there is any dynamic content inside the ion-content, be sure to call `.resize()` with {@link ionic.service:$ionicScrollDelegate}
    * after the content has been added.
    *
    * Be aware that this directive gets its own child scope. If you do not understand why this
    * is important, you can read [https://docs.angularjs.org/guide/scope](https://docs.angularjs.org/guide/scope).
    *
    * @param {string=} delegate-handle The handle used to identify this scrollView
    * with {@link ionic.service:$ionicScrollDelegate}.
    * @param {string=} direction Which way to scroll. 'x' or 'y' or 'xy'. Default 'y'.
    * @param {boolean=} locking Whether to lock scrolling in one direction at a time. Useful to set to false when zoomed in or scrolling in two directions. Default true.
    * @param {boolean=} padding Whether to add padding to the content.
    * Defaults to true on iOS, false on Android.
    * @param {boolean=} scroll Whether to allow scrolling of content.  Defaults to true.
    * @param {boolean=} overflow-scroll Whether to use overflow-scrolling instead of
    * Ionic scroll. See {@link ionic.provider:$ionicConfigProvider} to set this as the global default.
    * @param {boolean=} scrollbar-x Whether to show the horizontal scrollbar. Default true.
    * @param {boolean=} scrollbar-y Whether to show the vertical scrollbar. Default true.
    * @param {string=} start-x Initial horizontal scroll position. Default 0.
    * @param {string=} start-y Initial vertical scroll position. Default 0.
    * @param {expression=} on-scroll Expression to evaluate when the content is scrolled.
    * @param {expression=} on-scroll-complete Expression to evaluate when a scroll action completes. Has access to 'scrollLeft' and 'scrollTop' locals.
    * @param {boolean=} has-bouncing Whether to allow scrolling to bounce past the edges
    * of the content.  Defaults to true on iOS, false on Android.
    * @param {number=} scroll-event-interval Number of milliseconds between each firing of the 'on-scroll' expression. Default 10.
    */
    IonicModule
.directive('ionContent', [
  '$timeout',
  '$controller',
  '$ionicBind',
  '$ionicConfig',
function ($timeout, $controller, $ionicBind, $ionicConfig) {
    return {
        restrict: 'E',
        require: '^?ionNavView',
        scope: true,
        priority: 800,
        compile: function (element, attr) {
            var innerElement;
            var scrollCtrl;

            element.addClass('scroll-content ionic-scroll');

            if (attr.scroll != 'false') {
                //We cannot use normal transclude here because it breaks element.data()
                //inheritance on compile
                innerElement = jqLite('<div class="scroll"></div>');
                innerElement.append(element.contents());
                element.append(innerElement);
            } else {
                element.addClass('scroll-content-false');
            }

            var nativeScrolling = attr.overflowScroll !== "false" && (attr.overflowScroll === "true" || !$ionicConfig.scrolling.jsScrolling());

            // collection-repeat requires JS scrolling
            if (nativeScrolling) {
                nativeScrolling = !element[0].querySelector('[collection-repeat]');
            }
            return { pre: prelink };
            function prelink($scope, $element, $attr) {
                var parentScope = $scope.$parent;
                $scope.$watch(function () {
                    return (parentScope.$hasHeader ? ' has-header' : '') +
            (parentScope.$hasSubheader ? ' has-subheader' : '') +
            (parentScope.$hasFooter ? ' has-footer' : '') +
            (parentScope.$hasSubfooter ? ' has-subfooter' : '') +
            (parentScope.$hasTabs ? ' has-tabs' : '') +
            (parentScope.$hasTabsTop ? ' has-tabs-top' : '');
                }, function (className, oldClassName) {
                    $element.removeClass(oldClassName);
                    $element.addClass(className);
                });

                //Only this ionContent should use these variables from parent scopes
                $scope.$hasHeader = $scope.$hasSubheader =
          $scope.$hasFooter = $scope.$hasSubfooter =
          $scope.$hasTabs = $scope.$hasTabsTop =
          false;
                $ionicBind($scope, $attr, {
                    $onScroll: '&onScroll',
                    $onScrollComplete: '&onScrollComplete',
                    hasBouncing: '@',
                    padding: '@',
                    direction: '@',
                    scrollbarX: '@',
                    scrollbarY: '@',
                    startX: '@',
                    startY: '@',
                    scrollEventInterval: '@'
                });
                $scope.direction = $scope.direction || 'y';

                if (isDefined($attr.padding)) {
                    $scope.$watch($attr.padding, function (newVal) {
                        (innerElement || $element).toggleClass('padding', !!newVal);
                    });
                }

                if ($attr.scroll === "false") {
                    //do nothing
                } else {
                    var scrollViewOptions = {};

                    // determined in compile phase above
                    if (nativeScrolling) {
                        // use native scrolling
                        $element.addClass('overflow-scroll');

                        scrollViewOptions = {
                            el: $element[0],
                            delegateHandle: attr.delegateHandle,
                            startX: $scope.$eval($scope.startX) || 0,
                            startY: $scope.$eval($scope.startY) || 0,
                            nativeScrolling: true
                        };

                    } else {
                        // Use JS scrolling
                        scrollViewOptions = {
                            el: $element[0],
                            delegateHandle: attr.delegateHandle,
                            locking: (attr.locking || 'true') === 'true',
                            bouncing: $scope.$eval($scope.hasBouncing),
                            startX: $scope.$eval($scope.startX) || 0,
                            startY: $scope.$eval($scope.startY) || 0,
                            scrollbarX: $scope.$eval($scope.scrollbarX) !== false,
                            scrollbarY: $scope.$eval($scope.scrollbarY) !== false,
                            scrollingX: $scope.direction.indexOf('x') >= 0,
                            scrollingY: $scope.direction.indexOf('y') >= 0,
                            scrollEventInterval: parseInt($scope.scrollEventInterval, 10) || 10,
                            scrollingComplete: onScrollComplete
                        };
                    }

                    // init scroll controller with appropriate options
                    scrollCtrl = $controller('$ionicScroll', {
                        $scope: $scope,
                        scrollViewOptions: scrollViewOptions
                    });

                    $scope.scrollCtrl = scrollCtrl;

                    $scope.$on('$destroy', function () {
                        if (scrollViewOptions) {
                            scrollViewOptions.scrollingComplete = noop;
                            delete scrollViewOptions.el;
                        }
                        innerElement = null;
                        $element = null;
                        attr.$$element = null;
                    });
                }

                function onScrollComplete() {
                    $scope.$onScrollComplete({
                        scrollTop: scrollCtrl.scrollView.__scrollTop,
                        scrollLeft: scrollCtrl.scrollView.__scrollLeft
                    });
                }

            }
        }
    };
} ]);

    /**
    * @ngdoc directive
    * @name exposeAsideWhen
    * @module ionic
    * @restrict A
    * @parent ionic.directive:ionSideMenus
    *
    * @description
    * It is common for a tablet application to hide a menu when in portrait mode, but to show the
    * same menu on the left side when the tablet is in landscape mode. The `exposeAsideWhen` attribute
    * directive can be used to accomplish a similar interface.
    *
    * By default, side menus are hidden underneath its side menu content, and can be opened by either
    * swiping the content left or right, or toggling a button to show the side menu. However, by adding the
    * `exposeAsideWhen` attribute directive to an {@link ionic.directive:ionSideMenu} element directive,
    * a side menu can be given instructions on "when" the menu should be exposed (always viewable). For
    * example, the `expose-aside-when="large"` attribute will keep the side menu hidden when the viewport's
    * width is less than `768px`, but when the viewport's width is `768px` or greater, the menu will then
    * always be shown and can no longer be opened or closed like it could when it was hidden for smaller
    * viewports.
    *
    * Using `large` as the attribute's value is a shortcut value to `(min-width:768px)` since it is
    * the most common use-case. However, for added flexibility, any valid media query could be added
    * as the value, such as `(min-width:600px)` or even multiple queries such as
    * `(min-width:750px) and (max-width:1200px)`.
    * @usage
    * ```html
    * <ion-side-menus>
    *   <!-- Center content -->
    *   <ion-side-menu-content>
    *   </ion-side-menu-content>
    *
    *   <!-- Left menu -->
    *   <ion-side-menu expose-aside-when="large">
    *   </ion-side-menu>
    * </ion-side-menus>
    * ```
    * For a complete side menu example, see the
    * {@link ionic.directive:ionSideMenus} documentation.
    */

    IonicModule.directive('exposeAsideWhen', ['$window', function ($window) {
        return {
            restrict: 'A',
            require: '^ionSideMenus',
            link: function ($scope, $element, $attr, sideMenuCtrl) {

                var prevInnerWidth = $window.innerWidth;
                var prevInnerHeight = $window.innerHeight;

                ionic.on('resize', function () {
                    if (prevInnerWidth === $window.innerWidth && prevInnerHeight === $window.innerHeight) {
                        return;
                    }
                    prevInnerWidth = $window.innerWidth;
                    prevInnerHeight = $window.innerHeight;
                    onResize();
                }, $window);

                function checkAsideExpose() {
                    var mq = $attr.exposeAsideWhen == 'large' ? '(min-width:768px)' : $attr.exposeAsideWhen;
                    sideMenuCtrl.exposeAside($window.matchMedia(mq).matches);
                    sideMenuCtrl.activeAsideResizing(false);
                }

                function onResize() {
                    sideMenuCtrl.activeAsideResizing(true);
                    debouncedCheck();
                }

                var debouncedCheck = ionic.debounce(function () {
                    $scope.$apply(checkAsideExpose);
                }, 300, false);

                $scope.$evalAsync(checkAsideExpose);
            }
        };
    } ]);

    var GESTURE_DIRECTIVES = 'onHold onTap onDoubleTap onTouch onRelease onDragStart onDrag onDragEnd onDragUp onDragRight onDragDown onDragLeft onSwipe onSwipeUp onSwipeRight onSwipeDown onSwipeLeft'.split(' ');

    GESTURE_DIRECTIVES.forEach(function (name) {
        IonicModule.directive(name, gestureDirective(name));
    });


    /**
    * @ngdoc directive
    * @name onHold
    * @module ionic
    * @restrict A
    *
    * @description
    * Touch stays at the same location for 500ms. Similar to long touch events available for AngularJS and jQuery.
    *
    * @usage
    * ```html
    * <button on-hold="onHold()" class="button">Test</button>
    * ```
    */


    /**
    * @ngdoc directive
    * @name onTap
    * @module ionic
    * @restrict A
    *
    * @description
    * Quick touch at a location. If the duration of the touch goes
    * longer than 250ms it is no longer a tap gesture.
    *
    * @usage
    * ```html
    * <button on-tap="onTap()" class="button">Test</button>
    * ```
    */


    /**
    * @ngdoc directive
    * @name onDoubleTap
    * @module ionic
    * @restrict A
    *
    * @description
    * Double tap touch at a location.
    *
    * @usage
    * ```html
    * <button on-double-tap="onDoubleTap()" class="button">Test</button>
    * ```
    */


    /**
    * @ngdoc directive
    * @name onTouch
    * @module ionic
    * @restrict A
    *
    * @description
    * Called immediately when the user first begins a touch. This
    * gesture does not wait for a touchend/mouseup.
    *
    * @usage
    * ```html
    * <button on-touch="onTouch()" class="button">Test</button>
    * ```
    */


    /**
    * @ngdoc directive
    * @name onRelease
    * @module ionic
    * @restrict A
    *
    * @description
    * Called when the user ends a touch.
    *
    * @usage
    * ```html
    * <button on-release="onRelease()" class="button">Test</button>
    * ```
    */

    /**
    * @ngdoc directive
    * @name onDragStart
    * @module ionic
    * @restrict A
    *
    * @description
    * Called when a drag gesture has started.
    *
    * @usage
    * ```html
    * <button on-drag-start="onDragStart()" class="button">Test</button>
    * ```
    */


    /**
    * @ngdoc directive
    * @name onDrag
    * @module ionic
    * @restrict A
    *
    * @description
    * Move with one touch around on the page. Blocking the scrolling when
    * moving left and right is a good practice. When all the drag events are
    * blocking you disable scrolling on that area.
    *
    * @usage
    * ```html
    * <button on-drag="onDrag()" class="button">Test</button>
    * ```
    */

    /**
    * @ngdoc directive
    * @name onDragEnd
    * @module ionic
    * @restrict A
    *
    * @description
    * Called when a drag gesture has ended.
    *
    * @usage
    * ```html
    * <button on-drag-end="onDragEnd()" class="button">Test</button>
    * ```
    */

    /**
    * @ngdoc directive
    * @name onDragUp
    * @module ionic
    * @restrict A
    *
    * @description
    * Called when the element is dragged up.
    *
    * @usage
    * ```html
    * <button on-drag-up="onDragUp()" class="button">Test</button>
    * ```
    */


    /**
    * @ngdoc directive
    * @name onDragRight
    * @module ionic
    * @restrict A
    *
    * @description
    * Called when the element is dragged to the right.
    *
    * @usage
    * ```html
    * <button on-drag-right="onDragRight()" class="button">Test</button>
    * ```
    */


    /**
    * @ngdoc directive
    * @name onDragDown
    * @module ionic
    * @restrict A
    *
    * @description
    * Called when the element is dragged down.
    *
    * @usage
    * ```html
    * <button on-drag-down="onDragDown()" class="button">Test</button>
    * ```
    */


    /**
    * @ngdoc directive
    * @name onDragLeft
    * @module ionic
    * @restrict A
    *
    * @description
    * Called when the element is dragged to the left.
    *
    * @usage
    * ```html
    * <button on-drag-left="onDragLeft()" class="button">Test</button>
    * ```
    */


    /**
    * @ngdoc directive
    * @name onSwipe
    * @module ionic
    * @restrict A
    *
    * @description
    * Called when a moving touch has a high velocity in any direction.
    *
    * @usage
    * ```html
    * <button on-swipe="onSwipe()" class="button">Test</button>
    * ```
    */


    /**
    * @ngdoc directive
    * @name onSwipeUp
    * @module ionic
    * @restrict A
    *
    * @description
    * Called when a moving touch has a high velocity moving up.
    *
    * @usage
    * ```html
    * <button on-swipe-up="onSwipeUp()" class="button">Test</button>
    * ```
    */


    /**
    * @ngdoc directive
    * @name onSwipeRight
    * @module ionic
    * @restrict A
    *
    * @description
    * Called when a moving touch has a high velocity moving to the right.
    *
    * @usage
    * ```html
    * <button on-swipe-right="onSwipeRight()" class="button">Test</button>
    * ```
    */


    /**
    * @ngdoc directive
    * @name onSwipeDown
    * @module ionic
    * @restrict A
    *
    * @description
    * Called when a moving touch has a high velocity moving down.
    *
    * @usage
    * ```html
    * <button on-swipe-down="onSwipeDown()" class="button">Test</button>
    * ```
    */


    /**
    * @ngdoc directive
    * @name onSwipeLeft
    * @module ionic
    * @restrict A
    *
    * @description
    * Called when a moving touch has a high velocity moving to the left.
    *
    * @usage
    * ```html
    * <button on-swipe-left="onSwipeLeft()" class="button">Test</button>
    * ```
    */


    function gestureDirective(directiveName) {
        return ['$ionicGesture', '$parse', function ($ionicGesture, $parse) {
            var eventType = directiveName.substr(2).toLowerCase();

            return function (scope, element, attr) {
                var fn = $parse(attr[directiveName]);

                var listener = function (ev) {
                    scope.$apply(function () {
                        fn(scope, {
                            $event: ev
                        });
                    });
                };

                var gesture = $ionicGesture.on(eventType, listener, element);

                scope.$on('$destroy', function () {
                    $ionicGesture.off(gesture, eventType, listener);
                });
            };
        } ];
    }


    IonicModule
    //.directive('ionHeaderBar', tapScrollToTopDirective())

    /**
    * @ngdoc directive
    * @name ionHeaderBar
    * @module ionic
    * @restrict E
    *
    * @description
    * Adds a fixed header bar above some content.
    *
    * Can also be a subheader (lower down) if the 'bar-subheader' class is applied.
    * See [the header CSS docs](/docs/components/#subheader).
    *
    * @param {string=} align-title How to align the title. By default the title
    * will be aligned the same as how the platform aligns its titles (iOS centers
    * titles, Android aligns them left).
    * Available: 'left', 'right', or 'center'.  Defaults to the same as the platform.
    * @param {boolean=} no-tap-scroll By default, the header bar will scroll the
    * content to the top when tapped.  Set no-tap-scroll to true to disable this
    * behavior.
    * Available: true or false.  Defaults to false.
    *
    * @usage
    * ```html
    * <ion-header-bar align-title="left" class="bar-positive">
    *   <div class="buttons">
    *     <button class="button" ng-click="doSomething()">Left Button</button>
    *   </div>
    *   <h1 class="title">Title!</h1>
    *   <div class="buttons">
    *     <button class="button">Right Button</button>
    *   </div>
    * </ion-header-bar>
    * <ion-content class="has-header">
    *   Some content!
    * </ion-content>
    * ```
    */
.directive('ionHeaderBar', headerFooterBarDirective(true))

    /**
    * @ngdoc directive
    * @name ionFooterBar
    * @module ionic
    * @restrict E
    *
    * @description
    * Adds a fixed footer bar below some content.
    *
    * Can also be a subfooter (higher up) if the 'bar-subfooter' class is applied.
    * See [the footer CSS docs](/docs/components/#footer).
    *
    * Note: If you use ionFooterBar in combination with ng-if, the surrounding content
    * will not align correctly.  This will be fixed soon.
    *
    * @param {string=} align-title Where to align the title.
    * Available: 'left', 'right', or 'center'.  Defaults to 'center'.
    *
    * @usage
    * ```html
    * <ion-content class="has-footer">
    *   Some content!
    * </ion-content>
    * <ion-footer-bar align-title="left" class="bar-assertive">
    *   <div class="buttons">
    *     <button class="button">Left Button</button>
    *   </div>
    *   <h1 class="title">Title!</h1>
    *   <div class="buttons" ng-click="doSomething()">
    *     <button class="button">Right Button</button>
    *   </div>
    * </ion-footer-bar>
    * ```
    */
.directive('ionFooterBar', headerFooterBarDirective(false));

    function tapScrollToTopDirective() { //eslint-disable-line no-unused-vars
        return ['$ionicScrollDelegate', function ($ionicScrollDelegate) {
            return {
                restrict: 'E',
                link: function ($scope, $element, $attr) {
                    if ($attr.noTapScroll == 'true') {
                        return;
                    }
                    ionic.on('tap', onTap, $element[0]);
                    $scope.$on('$destroy', function () {
                        ionic.off('tap', onTap, $element[0]);
                    });

                    function onTap(e) {
                        var depth = 3;
                        var current = e.target;
                        //Don't scroll to top in certain cases
                        while (depth-- && current) {
                            if (current.classList.contains('button') ||
                current.tagName.match(/input|textarea|select/i) ||
                current.isContentEditable) {
                                return;
                            }
                            current = current.parentNode;
                        }
                        var touch = e.gesture && e.gesture.touches[0] || e.detail.touches[0];
                        var bounds = $element[0].getBoundingClientRect();
                        if (ionic.DomUtil.rectContains(
            touch.pageX, touch.pageY,
            bounds.left, bounds.top - 20,
            bounds.left + bounds.width, bounds.top + bounds.height
          )) {
                            $ionicScrollDelegate.scrollTop(true);
                        }
                    }
                }
            };
        } ];
    }

    function headerFooterBarDirective(isHeader) {
        return ['$document', '$timeout', function ($document, $timeout) {
            return {
                restrict: 'E',
                controller: '$ionicHeaderBar',
                compile: function (tElement) {
                    tElement.addClass(isHeader ? 'bar bar-header' : 'bar bar-footer');
                    // top style tabs? if so, remove bottom border for seamless display
                    $timeout(function () {
                        if (isHeader && $document[0].getElementsByClassName('tabs-top').length) tElement.addClass('has-tabs-top');
                    });

                    return { pre: prelink };
                    function prelink($scope, $element, $attr, ctrl) {
                        if (isHeader) {
                            $scope.$watch(function () { return $element[0].className; }, function (value) {
                                var isShown = value.indexOf('ng-hide') === -1;
                                var isSubheader = value.indexOf('bar-subheader') !== -1;
                                $scope.$hasHeader = isShown && !isSubheader;
                                $scope.$hasSubheader = isShown && isSubheader;
                                $scope.$emit('$ionicSubheader', $scope.$hasSubheader);
                            });
                            $scope.$on('$destroy', function () {
                                delete $scope.$hasHeader;
                                delete $scope.$hasSubheader;
                            });
                            ctrl.align();
                            $scope.$on('$ionicHeader.align', function () {
                                ionic.requestAnimationFrame(function () {
                                    ctrl.align();
                                });
                            });

                        } else {
                            $scope.$watch(function () { return $element[0].className; }, function (value) {
                                var isShown = value.indexOf('ng-hide') === -1;
                                var isSubfooter = value.indexOf('bar-subfooter') !== -1;
                                $scope.$hasFooter = isShown && !isSubfooter;
                                $scope.$hasSubfooter = isShown && isSubfooter;
                            });
                            $scope.$on('$destroy', function () {
                                delete $scope.$hasFooter;
                                delete $scope.$hasSubfooter;
                            });
                            $scope.$watch('$hasTabs', function (val) {
                                $element.toggleClass('has-tabs', !!val);
                            });
                            ctrl.align();
                            $scope.$on('$ionicFooter.align', function () {
                                ionic.requestAnimationFrame(function () {
                                    ctrl.align();
                                });
                            });
                        }
                    }
                }
            };
        } ];
    }

    /**
    * @ngdoc directive
    * @name ionInfiniteScroll
    * @module ionic
    * @parent ionic.directive:ionContent, ionic.directive:ionScroll
    * @restrict E
    *
    * @description
    * The ionInfiniteScroll directive allows you to call a function whenever
    * the user gets to the bottom of the page or near the bottom of the page.
    *
    * The expression you pass in for `on-infinite` is called when the user scrolls
    * greater than `distance` away from the bottom of the content.  Once `on-infinite`
    * is done loading new data, it should broadcast the `scroll.infiniteScrollComplete`
    * event from your controller (see below example).
    *
    * @param {expression} on-infinite What to call when the scroller reaches the
    * bottom.
    * @param {string=} distance The distance from the bottom that the scroll must
    * reach to trigger the on-infinite expression. Default: 1%.
    * @param {string=} spinner The {@link ionic.directive:ionSpinner} to show while loading. The SVG
    * {@link ionic.directive:ionSpinner} is now the default, replacing rotating font icons.
    * @param {string=} icon The icon to show while loading. Default: 'ion-load-d'.  This is depreicated
    * in favor of the SVG {@link ionic.directive:ionSpinner}.
    * @param {boolean=} immediate-check Whether to check the infinite scroll bounds immediately on load.
    *
    * @usage
    * ```html
    * <ion-content ng-controller="MyController">
    *   <ion-list>
    *   ....
    *   ....
    *   </ion-list>
    *
    *   <ion-infinite-scroll
    *     on-infinite="loadMore()"
    *     distance="1%">
    *   </ion-infinite-scroll>
    * </ion-content>
    * ```
    * ```js
    * function MyController($scope, $http) {
    *   $scope.items = [];
    *   $scope.loadMore = function() {
    *     $http.get('/more-items').success(function(items) {
    *       useItems(items);
    *       $scope.$broadcast('scroll.infiniteScrollComplete');
    *     });
    *   };
    *
    *   $scope.$on('$stateChangeSuccess', function() {
    *     $scope.loadMore();
    *   });
    * }
    * ```
    *
    * An easy to way to stop infinite scroll once there is no more data to load
    * is to use angular's `ng-if` directive:
    *
    * ```html
    * <ion-infinite-scroll
    *   ng-if="moreDataCanBeLoaded()"
    *   icon="ion-loading-c"
    *   on-infinite="loadMoreData()">
    * </ion-infinite-scroll>
    * ```
    */
    IonicModule
.directive('ionInfiniteScroll', ['$timeout', function ($timeout) {
    return {
        restrict: 'E',
        require: ['?^$ionicScroll', 'ionInfiniteScroll'],
        template: function ($element, $attrs) {
            if ($attrs.icon) return '<i class="icon {{icon()}} icon-refreshing {{scrollingType}}"></i>';
            return '<ion-spinner icon="{{spinner()}}"></ion-spinner>';
        },
        scope: true,
        controller: '$ionInfiniteScroll',
        link: function ($scope, $element, $attrs, ctrls) {
            var infiniteScrollCtrl = ctrls[1];
            var scrollCtrl = infiniteScrollCtrl.scrollCtrl = ctrls[0];
            var jsScrolling = infiniteScrollCtrl.jsScrolling = !scrollCtrl.isNative();

            // if this view is not beneath a scrollCtrl, it can't be injected, proceed w/ native scrolling
            if (jsScrolling) {
                infiniteScrollCtrl.scrollView = scrollCtrl.scrollView;
                $scope.scrollingType = 'js-scrolling';
                //bind to JS scroll events
                scrollCtrl.$element.on('scroll', infiniteScrollCtrl.checkBounds);
            } else {
                // grabbing the scrollable element, to determine dimensions, and current scroll pos
                var scrollEl = ionic.DomUtil.getParentOrSelfWithClass($element[0].parentNode, 'overflow-scroll');
                infiniteScrollCtrl.scrollEl = scrollEl;
                // if there's no scroll controller, and no overflow scroll div, infinite scroll wont work
                if (!scrollEl) {
                    throw 'Infinite scroll must be used inside a scrollable div';
                }
                //bind to native scroll events
                infiniteScrollCtrl.scrollEl.addEventListener('scroll', infiniteScrollCtrl.checkBounds);
            }

            // Optionally check bounds on start after scrollView is fully rendered
            var doImmediateCheck = isDefined($attrs.immediateCheck) ? $scope.$eval($attrs.immediateCheck) : true;
            if (doImmediateCheck) {
                $timeout(function () { infiniteScrollCtrl.checkBounds(); });
            }
        }
    };
} ]);

    /**
    * @ngdoc directive
    * @name ionInput
    * @parent ionic.directive:ionList
    * @module ionic
    * @restrict E
    * Creates a text input group that can easily be focused
    *
    * @usage
    *
    * ```html
    * <ion-list>
    *   <ion-input>
    *     <input type="text" placeholder="First Name">
    *   </ion-input>
    *
    *   <ion-input>
    *     <ion-label>Username</ion-label>
    *     <input type="text">
    *   </ion-input>
    * </ion-list>
    * ```
    */

    var labelIds = -1;

    IonicModule
.directive('ionInput', [function () {
    return {
        restrict: 'E',
        controller: ['$scope', '$element', function ($scope, $element) {
            this.$scope = $scope;
            this.$element = $element;

            this.setInputAriaLabeledBy = function (id) {
                var inputs = $element[0].querySelectorAll('input,textarea');
                inputs.length && inputs[0].setAttribute('aria-labelledby', id);
            };

            this.focus = function () {
                var inputs = $element[0].querySelectorAll('input,textarea');
                inputs.length && inputs[0].focus();
            };
        } ]
    };
} ]);

    /**
    * @ngdoc directive
    * @name ionLabel
    * @parent ionic.directive:ionList
    * @module ionic
    * @restrict E
    *
    * New in Ionic 1.2. It is strongly recommended that you use `<ion-label>` in place
    * of any `<label>` elements for maximum cross-browser support and performance.
    *
    * Creates a label for a form input.
    *
    * @usage
    *
    * ```html
    * <ion-list>
    *   <ion-input>
    *     <ion-label>Username</ion-label>
    *     <input type="text">
    *   </ion-input>
    * </ion-list>
    * ```
    */
    IonicModule
.directive('ionLabel', [function () {
    return {
        restrict: 'E',
        require: '?^ionInput',
        compile: function () {

            return function link($scope, $element, $attrs, ionInputCtrl) {
                var element = $element[0];

                $element.addClass('input-label');

                $element.attr('aria-label', $element.text());
                var id = element.id || '_label-' + ++labelIds;

                if (!element.id) {
                    $element.attr('id', id);
                }

                if (ionInputCtrl) {

                    ionInputCtrl.setInputAriaLabeledBy(id);

                    $element.on('click', function () {
                        ionInputCtrl.focus();
                    });
                }
            };
        }
    };
} ]);

    /**
    * Input label adds accessibility to <span class="input-label">.
    */
    IonicModule
.directive('inputLabel', [function () {
    return {
        restrict: 'C',
        require: '?^ionInput',
        compile: function () {

            return function link($scope, $element, $attrs, ionInputCtrl) {
                var element = $element[0];

                $element.attr('aria-label', $element.text());
                var id = element.id || '_label-' + ++labelIds;

                if (!element.id) {
                    $element.attr('id', id);
                }

                if (ionInputCtrl) {
                    ionInputCtrl.setInputAriaLabeledBy(id);
                }

            };
        }
    };
} ]);

    /**
    * @ngdoc directive
    * @name ionItem
    * @parent ionic.directive:ionList
    * @module ionic
    * @restrict E
    * Creates a list-item that can easily be swiped,
    * deleted, reordered, edited, and more.
    *
    * See {@link ionic.directive:ionList} for a complete example & explanation.
    *
    * Can be assigned any item class name. See the
    * [list CSS documentation](/docs/components/#list).
    *
    * @usage
    *
    * ```html
    * <ion-list>
    *   <ion-item>Hello!</ion-item>
    *   <ion-item href="#/detail">
    *     Link to detail page
    *   </ion-item>
    * </ion-list>
    * ```
    */
    IonicModule
.directive('ionItem', ['$$rAF', function ($$rAF) {
    return {
        restrict: 'E',
        controller: ['$scope', '$element', function ($scope, $element) {
            this.$scope = $scope;
            this.$element = $element;
        } ],
        scope: true,
        compile: function ($element, $attrs) {
            var isAnchor = isDefined($attrs.href) ||
                     isDefined($attrs.ngHref) ||
                     isDefined($attrs.uiSref);
            var isComplexItem = isAnchor ||
            //Lame way of testing, but we have to know at compile what to do with the element
        /ion-(delete|option|reorder)-button/i.test($element.html());

            if (isComplexItem) {
                var innerElement = jqLite(isAnchor ? '<a></a>' : '<div></div>');
                innerElement.addClass('item-content');

                if (isDefined($attrs.href) || isDefined($attrs.ngHref)) {
                    innerElement.attr('ng-href', '{{$href()}}');
                    if (isDefined($attrs.target)) {
                        innerElement.attr('target', '{{$target()}}');
                    }
                }

                innerElement.append($element.contents());

                $element.addClass('item item-complex')
                .append(innerElement);
            } else {
                $element.addClass('item');
            }

            return function link($scope, $element, $attrs) {
                $scope.$href = function () {
                    return $attrs.href || $attrs.ngHref;
                };
                $scope.$target = function () {
                    return $attrs.target;
                };

                var content = $element[0].querySelector('.item-content');
                if (content) {
                    $scope.$on('$collectionRepeatLeave', function () {
                        if (content && content.$$ionicOptionsOpen) {
                            content.style[ionic.CSS.TRANSFORM] = '';
                            content.style[ionic.CSS.TRANSITION] = 'none';
                            $$rAF(function () {
                                content.style[ionic.CSS.TRANSITION] = '';
                            });
                            content.$$ionicOptionsOpen = false;
                        }
                    });
                }
            };

        }
    };
} ]);

    var ITEM_TPL_DELETE_BUTTON =
  '<div class="item-left-edit item-delete enable-pointer-events">' +
  '</div>';
    /**
    * @ngdoc directive
    * @name ionDeleteButton
    * @parent ionic.directive:ionItem
    * @module ionic
    * @restrict E
    * Creates a delete button inside a list item, that is visible when the
    * {@link ionic.directive:ionList ionList parent's} `show-delete` evaluates to true or
    * `$ionicListDelegate.showDelete(true)` is called.
    *
    * Takes any ionicon as a class.
    *
    * See {@link ionic.directive:ionList} for a complete example & explanation.
    *
    * @usage
    *
    * ```html
    * <ion-list show-delete="shouldShowDelete">
    *   <ion-item>
    *     <ion-delete-button class="ion-minus-circled"></ion-delete-button>
    *     Hello, list item!
    *   </ion-item>
    * </ion-list>
    * <ion-toggle ng-model="shouldShowDelete">
    *   Show Delete?
    * </ion-toggle>
    * ```
    */
    IonicModule
.directive('ionDeleteButton', function () {

    function stopPropagation(ev) {
        ev.stopPropagation();
    }

    return {
        restrict: 'E',
        require: ['^^ionItem', '^?ionList'],
        //Run before anything else, so we can move it before other directives process
        //its location (eg ngIf relies on the location of the directive in the dom)
        priority: Number.MAX_VALUE,
        compile: function ($element, $attr) {
            //Add the classes we need during the compile phase, so that they stay
            //even if something else like ngIf removes the element and re-addss it
            $attr.$set('class', ($attr['class'] || '') + ' button icon button-icon', true);
            return function ($scope, $element, $attr, ctrls) {
                var itemCtrl = ctrls[0];
                var listCtrl = ctrls[1];
                var container = jqLite(ITEM_TPL_DELETE_BUTTON);
                container.append($element);
                itemCtrl.$element.append(container).addClass('item-left-editable');

                //Don't bubble click up to main .item
                $element.on('click', stopPropagation);

                init();
                $scope.$on('$ionic.reconnectScope', init);
                function init() {
                    listCtrl = listCtrl || $element.controller('ionList');
                    if (listCtrl && listCtrl.showDelete()) {
                        container.addClass('visible active');
                    }
                }
            };
        }
    };
});


    IonicModule
.directive('itemFloatingLabel', function () {
    return {
        restrict: 'C',
        link: function (scope, element) {
            var el = element[0];
            var input = el.querySelector('input, textarea');
            var inputLabel = el.querySelector('.input-label');

            if (!input || !inputLabel) return;

            var onInput = function () {
                if (input.value) {
                    inputLabel.classList.add('has-input');
                } else {
                    inputLabel.classList.remove('has-input');
                }
            };

            input.addEventListener('input', onInput);

            var ngModelCtrl = jqLite(input).controller('ngModel');
            if (ngModelCtrl) {
                ngModelCtrl.$render = function () {
                    input.value = ngModelCtrl.$viewValue || '';
                    onInput();
                };
            }

            scope.$on('$destroy', function () {
                input.removeEventListener('input', onInput);
            });
        }
    };
});

    var ITEM_TPL_OPTION_BUTTONS =
  '<div class="item-options invisible">' +
  '</div>';
    /**
    * @ngdoc directive
    * @name ionOptionButton
    * @parent ionic.directive:ionItem
    * @module ionic
    * @restrict E
    * @description
    * Creates an option button inside a list item, that is visible when the item is swiped
    * to the left by the user.  Swiped open option buttons can be hidden with
    * {@link ionic.service:$ionicListDelegate#closeOptionButtons $ionicListDelegate.closeOptionButtons}.
    *
    * Can be assigned any button class.
    *
    * See {@link ionic.directive:ionList} for a complete example & explanation.
    *
    * @usage
    *
    * ```html
    * <ion-list>
    *   <ion-item>
    *     I love kittens!
    *     <ion-option-button class="button-positive">Share</ion-option-button>
    *     <ion-option-button class="button-assertive">Edit</ion-option-button>
    *   </ion-item>
    * </ion-list>
    * ```
    */
    IonicModule.directive('ionOptionButton', [function () {
        function stopPropagation(e) {
            e.stopPropagation();
        }
        return {
            restrict: 'E',
            require: '^ionItem',
            priority: Number.MAX_VALUE,
            compile: function ($element, $attr) {
                $attr.$set('class', ($attr['class'] || '') + ' button', true);
                return function ($scope, $element, $attr, itemCtrl) {
                    if (!itemCtrl.optionsContainer) {
                        itemCtrl.optionsContainer = jqLite(ITEM_TPL_OPTION_BUTTONS);
                        itemCtrl.$element.prepend(itemCtrl.optionsContainer);
                    }
                    itemCtrl.optionsContainer.prepend($element);

                    itemCtrl.$element.addClass('item-right-editable');

                    //Don't bubble click up to main .item
                    $element.on('click', stopPropagation);
                };
            }
        };
    } ]);

    var ITEM_TPL_REORDER_BUTTON =
  '<div data-prevent-scroll="true" class="item-right-edit item-reorder enable-pointer-events">' +
  '</div>';

    /**
    * @ngdoc directive
    * @name ionReorderButton
    * @parent ionic.directive:ionItem
    * @module ionic
    * @restrict E
    * Creates a reorder button inside a list item, that is visible when the
    * {@link ionic.directive:ionList ionList parent's} `show-reorder` evaluates to true or
    * `$ionicListDelegate.showReorder(true)` is called.
    *
    * Can be dragged to reorder items in the list. Takes any ionicon class.
    *
    * Note: Reordering works best when used with `ng-repeat`.  Be sure that all `ion-item` children of an `ion-list` are part of the same `ng-repeat` expression.
    *
    * When an item reorder is complete, the expression given in the `on-reorder` attribute is called. The `on-reorder` expression is given two locals that can be used: `$fromIndex` and `$toIndex`.  See below for an example.
    *
    * Look at {@link ionic.directive:ionList} for more examples.
    *
    * @usage
    *
    * ```html
    * <ion-list ng-controller="MyCtrl" show-reorder="true">
    *   <ion-item ng-repeat="item in items">
    *     Item {{item}}
    *     <ion-reorder-button class="ion-navicon"
    *                         on-reorder="moveItem(item, $fromIndex, $toIndex)">
    *     </ion-reorder-button>
    *   </ion-item>
    * </ion-list>
    * ```
    * ```js
    * function MyCtrl($scope) {
    *   $scope.items = [1, 2, 3, 4];
    *   $scope.moveItem = function(item, fromIndex, toIndex) {
    *     //Move the item in the array
    *     $scope.items.splice(fromIndex, 1);
    *     $scope.items.splice(toIndex, 0, item);
    *   };
    * }
    * ```
    *
    * @param {expression=} on-reorder Expression to call when an item is reordered.
    * Parameters given: $fromIndex, $toIndex.
    */
    IonicModule
.directive('ionReorderButton', ['$parse', function ($parse) {
    return {
        restrict: 'E',
        require: ['^ionItem', '^?ionList'],
        priority: Number.MAX_VALUE,
        compile: function ($element, $attr) {
            $attr.$set('class', ($attr['class'] || '') + ' button icon button-icon', true);
            $element[0].setAttribute('data-prevent-scroll', true);
            return function ($scope, $element, $attr, ctrls) {
                var itemCtrl = ctrls[0];
                var listCtrl = ctrls[1];
                var onReorderFn = $parse($attr.onReorder);

                $scope.$onReorder = function (oldIndex, newIndex) {
                    onReorderFn($scope, {
                        $fromIndex: oldIndex,
                        $toIndex: newIndex
                    });
                };

                // prevent clicks from bubbling up to the item
                if (!$attr.ngClick && !$attr.onClick && !$attr.onclick) {
                    $element[0].onclick = function (e) {
                        e.stopPropagation();
                        return false;
                    };
                }

                var container = jqLite(ITEM_TPL_REORDER_BUTTON);
                container.append($element);
                itemCtrl.$element.append(container).addClass('item-right-editable');

                if (listCtrl && listCtrl.showReorder()) {
                    container.addClass('visible active');
                }
            };
        }
    };
} ]);

    /**
    * @ngdoc directive
    * @name keyboardAttach
    * @module ionic
    * @restrict A
    *
    * @description
    * keyboard-attach is an attribute directive which will cause an element to float above
    * the keyboard when the keyboard shows. Currently only supports the
    * [ion-footer-bar]({{ page.versionHref }}/api/directive/ionFooterBar/) directive.
    *
    * ### Notes
    * - This directive requires the
    * [Ionic Keyboard Plugin](https://github.com/driftyco/ionic-plugins-keyboard).
    * - On Android not in fullscreen mode, i.e. you have
    *   `<preference name="Fullscreen" value="false" />` or no preference in your `config.xml` file,
    *   this directive is unnecessary since it is the default behavior.
    * - On iOS, if there is an input in your footer, you will need to set
    *   `cordova.plugins.Keyboard.disableScroll(true)`.
    *
    * @usage
    *
    * ```html
    *  <ion-footer-bar align-title="left" keyboard-attach class="bar-assertive">
    *    <h1 class="title">Title!</h1>
    *  </ion-footer-bar>
    * ```
    */

    IonicModule
.directive('keyboardAttach', function () {
    return function (scope, element) {
        ionic.on('native.keyboardshow', onShow, window);
        ionic.on('native.keyboardhide', onHide, window);

        //deprecated
        ionic.on('native.showkeyboard', onShow, window);
        ionic.on('native.hidekeyboard', onHide, window);


        var scrollCtrl;

        function onShow(e) {
            if (ionic.Platform.isAndroid() && !ionic.Platform.isFullScreen) {
                return;
            }

            //for testing
            var keyboardHeight = e.keyboardHeight || (e.detail && e.detail.keyboardHeight);
            element.css('bottom', keyboardHeight + "px");
            scrollCtrl = element.controller('$ionicScroll');
            if (scrollCtrl) {
                scrollCtrl.scrollView.__container.style.bottom = keyboardHeight + keyboardAttachGetClientHeight(element[0]) + "px";
            }
        }

        function onHide() {
            if (ionic.Platform.isAndroid() && !ionic.Platform.isFullScreen) {
                return;
            }

            element.css('bottom', '');
            if (scrollCtrl) {
                scrollCtrl.scrollView.__container.style.bottom = '';
            }
        }

        scope.$on('$destroy', function () {
            ionic.off('native.keyboardshow', onShow, window);
            ionic.off('native.keyboardhide', onHide, window);

            //deprecated
            ionic.off('native.showkeyboard', onShow, window);
            ionic.off('native.hidekeyboard', onHide, window);
        });
    };
});

    function keyboardAttachGetClientHeight(element) {
        return element.clientHeight;
    }

    IonicModule.directive('ionList', [
  '$timeout',
function ($timeout) {
    return {
        restrict: 'E',
        require: ['ionList', '^?$ionicScroll'],
        controller: '$ionicList',
        compile: function ($element, $attr) {
            var listEl = jqLite('<div class="list">')
        .append($element.contents())
        .addClass($attr.type);

            $element.append(listEl);

            return function ($scope, $element, $attrs, ctrls) {
                var listCtrl = ctrls[0];
                var scrollCtrl = ctrls[1];

                // Wait for child elements to render...
                $timeout(init);

                function init() {
                    var listView = listCtrl.listView = new ionic.views.ListView({
                        el: $element[0],
                        listEl: $element.children()[0],
                        scrollEl: scrollCtrl && scrollCtrl.element,
                        scrollView: scrollCtrl && scrollCtrl.scrollView,
                        onReorder: function (el, oldIndex, newIndex) {
                            var itemScope = jqLite(el).scope();
                            if (itemScope && itemScope.$onReorder) {
                                // Make sure onReorder is called in apply cycle,
                                // but also make sure it has no conflicts by doing
                                // $evalAsync
                                $timeout(function () {
                                    itemScope.$onReorder(oldIndex, newIndex);
                                });
                            }
                        },
                        canSwipe: function () {
                            return listCtrl.canSwipeItems();
                        }
                    });

                    $scope.$on('$destroy', function () {
                        if (listView) {
                            listView.deregister && listView.deregister();
                            listView = null;
                        }
                    });

                    if (isDefined($attr.canSwipe)) {
                        $scope.$watch('!!(' + $attr.canSwipe + ')', function (value) {
                            listCtrl.canSwipeItems(value);
                        });
                    }
                    if (isDefined($attr.showDelete)) {
                        $scope.$watch('!!(' + $attr.showDelete + ')', function (value) {
                            listCtrl.showDelete(value);
                        });
                    }
                    if (isDefined($attr.showReorder)) {
                        $scope.$watch('!!(' + $attr.showReorder + ')', function (value) {
                            listCtrl.showReorder(value);
                        });
                    }

                    $scope.$watch(function () {
                        return listCtrl.showDelete();
                    }, function (isShown, wasShown) {
                        //Only use isShown=false if it was already shown
                        if (!isShown && !wasShown) { return; }

                        if (isShown) listCtrl.closeOptionButtons();
                        listCtrl.canSwipeItems(!isShown);

                        $element.children().toggleClass('list-left-editing', isShown);
                        $element.toggleClass('disable-pointer-events', isShown);

                        var deleteButton = jqLite($element[0].getElementsByClassName('item-delete'));
                        setButtonShown(deleteButton, listCtrl.showDelete);
                    });

                    $scope.$watch(function () {
                        return listCtrl.showReorder();
                    }, function (isShown, wasShown) {
                        //Only use isShown=false if it was already shown
                        if (!isShown && !wasShown) { return; }

                        if (isShown) listCtrl.closeOptionButtons();
                        listCtrl.canSwipeItems(!isShown);

                        $element.children().toggleClass('list-right-editing', isShown);
                        $element.toggleClass('disable-pointer-events', isShown);

                        var reorderButton = jqLite($element[0].getElementsByClassName('item-reorder'));
                        setButtonShown(reorderButton, listCtrl.showReorder);
                    });

                    function setButtonShown(el, shown) {
                        shown() && el.addClass('visible') || el.removeClass('active');
                        ionic.requestAnimationFrame(function () {
                            shown() && el.addClass('active') || el.removeClass('visible');
                        });
                    }
                }

            };
        }
    };
} ]);

    IonicModule.directive('menuClose', ['$ionicHistory', '$timeout', function ($ionicHistory, $timeout) {
        return {
            restrict: 'AC',
            link: function ($scope, $element) {
                $element.bind('click', function () {
                    var sideMenuCtrl = $element.inheritedData('$ionSideMenusController');
                    if (sideMenuCtrl) {
                        $ionicHistory.nextViewOptions({
                            historyRoot: true,
                            disableAnimate: true,
                            expire: 300
                        });
                        // if no transition in 300ms, reset nextViewOptions
                        // the expire should take care of it, but will be cancelled in some
                        // cases. This directive is an exception to the rules of history.js
                        $timeout(function () {
                            $ionicHistory.nextViewOptions({
                                historyRoot: false,
                                disableAnimate: false
                            });
                        }, 300);
                        sideMenuCtrl.close();
                    }
                });
            }
        };
    } ]);


    IonicModule.directive('menuToggle', function () {
        return {
            restrict: 'AC',
            link: function ($scope, $element, $attr) {
                $scope.$on('$ionicView.beforeEnter', function (ev, viewData) {
                    if (viewData.enableBack) {
                        var sideMenuCtrl = $element.inheritedData('$ionSideMenusController');
                        if (!sideMenuCtrl.enableMenuWithBackViews()) {
                            $element.addClass('hide');
                        }
                    } else {
                        $element.removeClass('hide');
                    }
                });

                $element.bind('click', function () {
                    var sideMenuCtrl = $element.inheritedData('$ionSideMenusController');
                    sideMenuCtrl && sideMenuCtrl.toggle($attr.menuToggle);
                });
            }
        };
    });

    /*
    * We don't document the ionModal directive, we instead document
    * the $ionicModal service
    */
    IonicModule.directive('ionModal', [function () {
        return {
            restrict: 'E',
            transclude: true,
            replace: true,
            controller: [function () { } ],
            template: '<div class="modal-backdrop">' +
                '<div class="modal-backdrop-bg"></div>' +
                '<div class="modal-wrapper" ng-transclude></div>' +
              '</div>'
        };
    } ]);

    IonicModule
.directive('ionModalView', function () {
    return {
        restrict: 'E',
        compile: function (element) {
            element.addClass('modal');
        }
    };
});


    IonicModule.directive('ionNavBackButton', ['$ionicConfig', '$document', function ($ionicConfig, $document) {
        return {
            restrict: 'E',
            require: '^ionNavBar',
            compile: function (tElement, tAttrs) {

                // clone the back button, but as a <div>
                var buttonEle = $document[0].createElement('button');
                for (var n in tAttrs.$attr) {
                    buttonEle.setAttribute(tAttrs.$attr[n], tAttrs[n]);
                }

                if (!tAttrs.ngClick) {
                    buttonEle.setAttribute('ng-click', '$ionicGoBack()');
                }

                buttonEle.className = 'button back-button hide buttons ' + (tElement.attr('class') || '');
                buttonEle.innerHTML = tElement.html() || '';

                var childNode;
                var hasIcon = hasIconClass(tElement[0]);
                var hasInnerText;
                var hasButtonText;
                var hasPreviousTitle;

                for (var x = 0; x < tElement[0].childNodes.length; x++) {
                    childNode = tElement[0].childNodes[x];
                    if (childNode.nodeType === 1) {
                        if (hasIconClass(childNode)) {
                            hasIcon = true;
                        } else if (childNode.classList.contains('default-title')) {
                            hasButtonText = true;
                        } else if (childNode.classList.contains('previous-title')) {
                            hasPreviousTitle = true;
                        }
                    } else if (!hasInnerText && childNode.nodeType === 3) {
                        hasInnerText = !!childNode.nodeValue.trim();
                    }
                }

                function hasIconClass(ele) {
                    return /ion-|icon/.test(ele.className);
                }

                var defaultIcon = $ionicConfig.backButton.icon();
                if (!hasIcon && defaultIcon && defaultIcon !== 'none') {
                    buttonEle.innerHTML = '<i class="icon ' + defaultIcon + '"></i> ' + buttonEle.innerHTML;
                    buttonEle.className += ' button-clear';
                }

                if (!hasInnerText) {
                    var buttonTextEle = $document[0].createElement('span');
                    buttonTextEle.className = 'back-text';

                    if (!hasButtonText && $ionicConfig.backButton.text()) {
                        buttonTextEle.innerHTML += '<span class="default-title">' + $ionicConfig.backButton.text() + '</span>';
                    }
                    if (!hasPreviousTitle && $ionicConfig.backButton.previousTitleText()) {
                        buttonTextEle.innerHTML += '<span class="previous-title"></span>';
                    }
                    buttonEle.appendChild(buttonTextEle);

                }

                tElement.attr('class', 'hide');
                tElement.empty();

                return {
                    pre: function ($scope, $element, $attr, navBarCtrl) {
                        // only register the plain HTML, the navBarCtrl takes care of scope/compile/link
                        navBarCtrl.navElement('backButton', buttonEle.outerHTML);
                        buttonEle = null;
                    }
                };
            }
        };
    } ]);



    IonicModule.directive('ionNavBar', function () {
        return {
            restrict: 'E',
            controller: '$ionicNavBar',
            scope: true,
            link: function ($scope, $element, $attr, ctrl) {
                ctrl.init();
            }
        };
    });



    IonicModule.directive('ionNavButtons', ['$document', function ($document) {
        return {
            require: '^ionNavBar',
            restrict: 'E',
            compile: function (tElement, tAttrs) {
                var side = 'left';

                if (/^primary|secondary|right$/i.test(tAttrs.side || '')) {
                    side = tAttrs.side.toLowerCase();
                }

                var spanEle = $document[0].createElement('span');
                spanEle.className = side + '-buttons';
                spanEle.innerHTML = tElement.html();

                var navElementType = side + 'Buttons';

                tElement.attr('class', 'hide');
                tElement.empty();

                return {
                    pre: function ($scope, $element, $attrs, navBarCtrl) {
                        // only register the plain HTML, the navBarCtrl takes care of scope/compile/link

                        var parentViewCtrl = $element.parent().data('$ionViewController');
                        if (parentViewCtrl) {
                            // if the parent is an ion-view, then these are ion-nav-buttons for JUST this ion-view
                            parentViewCtrl.navElement(navElementType, spanEle.outerHTML);

                        } else {
                            // these are buttons for all views that do not have their own ion-nav-buttons
                            navBarCtrl.navElement(navElementType, spanEle.outerHTML);
                        }

                        spanEle = null;
                    }
                };
            }
        };
    } ]);


    IonicModule.directive('navDirection', ['$ionicViewSwitcher', function ($ionicViewSwitcher) {
        return {
            restrict: 'A',
            priority: 1000,
            link: function ($scope, $element, $attr) {
                $element.bind('click', function () {
                    $ionicViewSwitcher.nextDirection($attr.navDirection);
                });
            }
        };
    } ]);


    IonicModule.directive('ionNavTitle', ['$document', function ($document) {
        return {
            require: '^ionNavBar',
            restrict: 'E',
            compile: function (tElement, tAttrs) {
                var navElementType = 'title';
                var spanEle = $document[0].createElement('span');
                for (var n in tAttrs.$attr) {
                    spanEle.setAttribute(tAttrs.$attr[n], tAttrs[n]);
                }
                spanEle.classList.add('nav-bar-title');
                spanEle.innerHTML = tElement.html();

                tElement.attr('class', 'hide');
                tElement.empty();

                return {
                    pre: function ($scope, $element, $attrs, navBarCtrl) {
                        // only register the plain HTML, the navBarCtrl takes care of scope/compile/link

                        var parentViewCtrl = $element.parent().data('$ionViewController');
                        if (parentViewCtrl) {
                            // if the parent is an ion-view, then these are ion-nav-buttons for JUST this ion-view
                            parentViewCtrl.navElement(navElementType, spanEle.outerHTML);

                        } else {
                            // these are buttons for all views that do not have their own ion-nav-buttons
                            navBarCtrl.navElement(navElementType, spanEle.outerHTML);
                        }

                        spanEle = null;
                    }
                };
            }
        };
    } ]);

    IonicModule.directive('navTransition', ['$ionicViewSwitcher', function ($ionicViewSwitcher) {
        return {
            restrict: 'A',
            priority: 1000,
            link: function ($scope, $element, $attr) {
                $element.bind('click', function () {
                    $ionicViewSwitcher.nextTransition($attr.navTransition);
                });
            }
        };
    } ]);


    IonicModule.directive('ionNavView', [
  '$state',
  '$ionicConfig',
function ($state, $ionicConfig) {
    // IONIC's fork of Angular UI Router, v0.2.10
    // the navView handles registering views in the history and how to transition between them
    return {
        restrict: 'E',
        terminal: true,
        priority: 2000,
        transclude: true,
        controller: '$ionicNavView',
        compile: function (tElement, tAttrs, transclude) {

            // a nav view element is a container for numerous views
            tElement.addClass('view-container');
            ionic.DomUtil.cachedAttr(tElement, 'nav-view-transition', $ionicConfig.views.transition());

            return function ($scope, $element, $attr, navViewCtrl) {
                var latestLocals;

                // Put in the compiled initial view
                transclude($scope, function (clone) {
                    $element.append(clone);
                });

                var viewData = navViewCtrl.init();

                // listen for $stateChangeSuccess
                $scope.$on('$stateChangeSuccess', function () {
                    updateView(false);
                });
                $scope.$on('$viewContentLoading', function () {
                    updateView(false);
                });

                // initial load, ready go
                updateView(true);


                function updateView(firstTime) {
                    // get the current local according to the $state
                    var viewLocals = $state.$current && $state.$current.locals[viewData.name];

                    // do not update THIS nav-view if its is not the container for the given state
                    // if the viewLocals are the same as THIS latestLocals, then nothing to do
                    if (!viewLocals || (!firstTime && viewLocals === latestLocals)) return;

                    // update the latestLocals
                    latestLocals = viewLocals;
                    viewData.state = viewLocals.$$state;

                    // register, update and transition to the new view
                    navViewCtrl.register(viewLocals);
                }

            };
        }
    };
} ]);

    IonicModule

.config(['$provide', function ($provide) {
    $provide.decorator('ngClickDirective', ['$delegate', function ($delegate) {
        // drop the default ngClick directive
        $delegate.shift();
        return $delegate;
    } ]);
} ])

    /**
    * @private
    */
.factory('$ionicNgClick', ['$parse', function ($parse) {
    return function (scope, element, clickExpr) {
        var clickHandler = angular.isFunction(clickExpr) ?
      clickExpr :
      $parse(clickExpr);

        element.on('click', function (event) {
            scope.$apply(function () {
                clickHandler(scope, { $event: (event) });
            });
        });

        // Hack for iOS Safari's benefit. It goes searching for onclick handlers and is liable to click
        // something else nearby.
        element.onclick = noop;
    };
} ])

.directive('ngClick', ['$ionicNgClick', function ($ionicNgClick) {
    return function (scope, element, attr) {
        $ionicNgClick(scope, element, attr.ngClick);
    };
} ])

.directive('ionStopEvent', function () {
    return {
        restrict: 'A',
        link: function (scope, element, attr) {
            element.bind(attr.ionStopEvent, eventStopPropagation);
        }
    };
});
    function eventStopPropagation(e) {
        e.stopPropagation();
    }


    /**
    * @ngdoc directive
    * @name ionPane
    * @module ionic
    * @restrict E
    *
    * @description A simple container that fits content, with no side effects.  Adds the 'pane' class to the element.
    */
    IonicModule
.directive('ionPane', function () {
    return {
        restrict: 'E',
        link: function (scope, element) {
            element.addClass('pane');
        }
    };
});

    /*
    * We don't document the ionPopover directive, we instead document
    * the $ionicPopover service
    */
    IonicModule
.directive('ionPopover', [function () {
    return {
        restrict: 'E',
        transclude: true,
        replace: true,
        controller: [function () { } ],
        template: '<div class="popover-backdrop">' +
                '<div class="popover-wrapper" ng-transclude></div>' +
              '</div>'
    };
} ]);

    IonicModule
.directive('ionPopoverView', function () {
    return {
        restrict: 'E',
        compile: function (element) {
            element.append(jqLite('<div class="popover-arrow">'));
            element.addClass('popover');
        }
    };
});


    IonicModule.directive('ionRadio', function () {
        return {
            restrict: 'E',
            replace: true,
            require: '?ngModel',
            transclude: true,
            template:
      '<label class="item item-radio">' +
        '<input type="radio" name="radio-group">' +
        '<div class="radio-content">' +
          '<div class="item-content disable-pointer-events" ng-transclude></div>' +
          '<i class="radio-icon disable-pointer-events icon ion-checkmark"></i>' +
        '</div>' +
      '</label>',

            compile: function (element, attr) {
                if (attr.icon) {
                    var iconElm = element.find('i');
                    iconElm.removeClass('ion-checkmark').addClass(attr.icon);
                }

                var input = element.find('input');
                forEach({
                    'name': attr.name,
                    'value': attr.value,
                    'disabled': attr.disabled,
                    'ng-value': attr.ngValue,
                    'ng-model': attr.ngModel,
                    'ng-disabled': attr.ngDisabled,
                    'ng-change': attr.ngChange,
                    'ng-required': attr.ngRequired,
                    'required': attr.required
                }, function (value, name) {
                    if (isDefined(value)) {
                        input.attr(name, value);
                    }
                });

                return function (scope, element, attr) {
                    scope.getValue = function () {
                        return scope.ngValue || attr.value;
                    };
                };
            }
        };
    });



    IonicModule.directive('ionRefresher', [function () {
        return {
            restrict: 'E',
            replace: true,
            require: ['?^$ionicScroll', 'ionRefresher'],
            controller: '$ionicRefresher',
            template:
    '<div class="scroll-refresher invisible" collection-repeat-ignore>' +
      '<div class="ionic-refresher-content" ' +
      'ng-class="{\'ionic-refresher-with-text\': pullingText || refreshingText}">' +
        '<div class="icon-pulling" ng-class="{\'pulling-rotation-disabled\':disablePullingRotation}">' +
          '<i class="icon {{pullingIcon}}"></i>' +
        '</div>' +
        '<div class="text-pulling" ng-bind-html="pullingText"></div>' +
        '<div class="icon-refreshing">' +
          '<ion-spinner ng-if="showSpinner" icon="{{spinner}}"></ion-spinner>' +
          '<i ng-if="showIcon" class="icon {{refreshingIcon}}"></i>' +
        '</div>' +
        '<div class="text-refreshing" ng-bind-html="refreshingText"></div>' +
      '</div>' +
    '</div>',
            link: function ($scope, $element, $attrs, ctrls) {

                // JS Scrolling uses the scroll controller
                var scrollCtrl = ctrls[0],
          refresherCtrl = ctrls[1];
                if (!scrollCtrl || scrollCtrl.isNative()) {
                    // Kick off native scrolling
                    refresherCtrl.init();
                } else {
                    $element[0].classList.add('js-scrolling');
                    scrollCtrl._setRefresher(
          $scope,
          $element[0],
          refresherCtrl.getRefresherDomMethods()
        );

                    $scope.$on('scroll.refreshComplete', function () {
                        $scope.$evalAsync(function () {
                            if (scrollCtrl.scrollView) {
                                scrollCtrl.scrollView.finishPullToRefresh();
                            }
                        });
                    });
                }

            }
        };
    } ]);


    IonicModule.directive('ionScroll', [
  '$timeout',
  '$controller',
  '$ionicBind',
  '$ionicConfig',
function ($timeout, $controller, $ionicBind, $ionicConfig) {
    return {
        restrict: 'E',
        scope: true,
        controller: function () { },
        compile: function (element, attr) {
            element.addClass('scroll-view ionic-scroll');

            //We cannot transclude here because it breaks element.data() inheritance on compile
            var innerElement = jqLite('<div class="scroll"></div>');
            innerElement.append(element.contents());
            element.append(innerElement);

            var nativeScrolling = attr.overflowScroll !== "false" && (attr.overflowScroll === "true" || !$ionicConfig.scrolling.jsScrolling());

            return { pre: prelink };
            function prelink($scope, $element, $attr) {
                $ionicBind($scope, $attr, {
                    direction: '@',
                    paging: '@',
                    $onScroll: '&onScroll',
                    $onScrollComplete: '&onScrollComplete',
                    scroll: '@',
                    scrollbarX: '@',
                    scrollbarY: '@',
                    zooming: '@',
                    minZoom: '@',
                    maxZoom: '@'
                });
                $scope.direction = $scope.direction || 'y';

                if (isDefined($attr.padding)) {
                    $scope.$watch($attr.padding, function (newVal) {
                        innerElement.toggleClass('padding', !!newVal);
                    });
                }
                if ($scope.$eval($scope.paging) === true) {
                    innerElement.addClass('scroll-paging');
                }

                if (!$scope.direction) { $scope.direction = 'y'; }
                var isPaging = $scope.$eval($scope.paging) === true;

                if (nativeScrolling) {
                    $element.addClass('overflow-scroll');
                }

                $element.addClass('scroll-' + $scope.direction);

                var scrollViewOptions = {
                    el: $element[0],
                    delegateHandle: $attr.delegateHandle,
                    locking: ($attr.locking || 'true') === 'true',
                    bouncing: $scope.$eval($attr.hasBouncing),
                    paging: isPaging,
                    scrollbarX: $scope.$eval($scope.scrollbarX) !== false,
                    scrollbarY: $scope.$eval($scope.scrollbarY) !== false,
                    scrollingX: $scope.direction.indexOf('x') >= 0,
                    scrollingY: $scope.direction.indexOf('y') >= 0,
                    zooming: $scope.$eval($scope.zooming) === true,
                    maxZoom: $scope.$eval($scope.maxZoom) || 3,
                    minZoom: $scope.$eval($scope.minZoom) || 0.5,
                    preventDefault: true,
                    nativeScrolling: nativeScrolling,
                    scrollingComplete: onScrollComplete
                };

                if (isPaging) {
                    scrollViewOptions.speedMultiplier = 0.8;
                    scrollViewOptions.bouncing = false;
                }

                var scrollCtrl = $controller('$ionicScroll', {
                    $scope: $scope,
                    scrollViewOptions: scrollViewOptions
                });

                function onScrollComplete() {
                    $scope.$onScrollComplete && $scope.$onScrollComplete({
                        scrollTop: scrollCtrl.scrollView.__scrollTop,
                        scrollLeft: scrollCtrl.scrollView.__scrollLeft
                    });
                }
            }
        }
    };
} ]);

    /**
    * @ngdoc directive
    * @name ionSideMenu
    * @module ionic
    * @restrict E
    * @parent ionic.directive:ionSideMenus
    *
    * @description
    * A container for a side menu, sibling to an {@link ionic.directive:ionSideMenuContent} directive.
    *
    * @usage
    * ```html
    * <ion-side-menu
    *   side="left"
    *   width="myWidthValue + 20"
    *   is-enabled="shouldLeftSideMenuBeEnabled()">
    * </ion-side-menu>
    * ```
    * For a complete side menu example, see the
    * {@link ionic.directive:ionSideMenus} documentation.
    *
    * @param {string} side Which side the side menu is currently on.  Allowed values: 'left' or 'right'.
    * @param {boolean=} is-enabled Whether this side menu is enabled.
    * @param {number=} width How many pixels wide the side menu should be.  Defaults to 275.
    */
    IonicModule
.directive('ionSideMenu', function () {
    return {
        restrict: 'E',
        require: '^ionSideMenus',
        scope: true,
        compile: function (element, attr) {
            angular.isUndefined(attr.isEnabled) && attr.$set('isEnabled', 'true');
            angular.isUndefined(attr.width) && attr.$set('width', '275');

            element.addClass('menu menu-' + attr.side);

            return function ($scope, $element, $attr, sideMenuCtrl) {
                $scope.side = $attr.side || 'left';

                var sideMenu = sideMenuCtrl[$scope.side] = new ionic.views.SideMenu({
                    width: attr.width,
                    el: $element[0],
                    isEnabled: true
                });

                $scope.$watch($attr.width, function (val) {
                    var numberVal = +val;
                    if (numberVal && numberVal == val) {
                        sideMenu.setWidth(+val);
                    }
                });
                $scope.$watch($attr.isEnabled, function (val) {
                    sideMenu.setIsEnabled(!!val);
                });
            };
        }
    };
});


    /**
    * @ngdoc directive
    * @name ionSideMenuContent
    * @module ionic
    * @restrict E
    * @parent ionic.directive:ionSideMenus
    *
    * @description
    * A container for the main visible content, sibling to one or more
    * {@link ionic.directive:ionSideMenu} directives.
    *
    * @usage
    * ```html
    * <ion-side-menu-content
    *   edge-drag-threshold="true"
    *   drag-content="true">
    * </ion-side-menu-content>
    * ```
    * For a complete side menu example, see the
    * {@link ionic.directive:ionSideMenus} documentation.
    *
    * @param {boolean=} drag-content Whether the content can be dragged. Default true.
    * @param {boolean|number=} edge-drag-threshold Whether the content drag can only start if it is below a certain threshold distance from the edge of the screen.  Default false. Accepts three types of values:
    *  - If a non-zero number is given, that many pixels is used as the maximum allowed distance from the edge that starts dragging the side menu.
    *  - If true is given, the default number of pixels (25) is used as the maximum allowed distance.
    *  - If false or 0 is given, the edge drag threshold is disabled, and dragging from anywhere on the content is allowed.
    *
    */
    IonicModule
.directive('ionSideMenuContent', [
  '$timeout',
  '$ionicGesture',
  '$window',
function ($timeout, $ionicGesture, $window) {

    return {
        restrict: 'EA', //DEPRECATED 'A'
        require: '^ionSideMenus',
        scope: true,
        compile: function (element, attr) {
            element.addClass('menu-content pane');

            return { pre: prelink };
            function prelink($scope, $element, $attr, sideMenuCtrl) {
                var startCoord = null;
                var primaryScrollAxis = null;

                if (isDefined(attr.dragContent)) {
                    $scope.$watch(attr.dragContent, function (value) {
                        sideMenuCtrl.canDragContent(value);
                    });
                } else {
                    sideMenuCtrl.canDragContent(true);
                }

                if (isDefined(attr.edgeDragThreshold)) {
                    $scope.$watch(attr.edgeDragThreshold, function (value) {
                        sideMenuCtrl.edgeDragThreshold(value);
                    });
                }

                // Listen for taps on the content to close the menu
                function onContentTap(gestureEvt) {
                    if (sideMenuCtrl.getOpenAmount() !== 0) {
                        sideMenuCtrl.close();
                        gestureEvt.gesture.srcEvent.preventDefault();
                        startCoord = null;
                        primaryScrollAxis = null;
                    } else if (!startCoord) {
                        startCoord = ionic.tap.pointerCoord(gestureEvt.gesture.srcEvent);
                    }
                }

                function onDragX(e) {
                    if (!sideMenuCtrl.isDraggableTarget(e)) return;

                    if (getPrimaryScrollAxis(e) == 'x') {
                        sideMenuCtrl._handleDrag(e);
                        e.gesture.srcEvent.preventDefault();
                    }
                }

                function onDragY(e) {
                    if (getPrimaryScrollAxis(e) == 'x') {
                        e.gesture.srcEvent.preventDefault();
                    }
                }

                function onDragRelease(e) {
                    sideMenuCtrl._endDrag(e);
                    startCoord = null;
                    primaryScrollAxis = null;
                }

                function getPrimaryScrollAxis(gestureEvt) {
                    // gets whether the user is primarily scrolling on the X or Y
                    // If a majority of the drag has been on the Y since the start of
                    // the drag, but the X has moved a little bit, it's still a Y drag

                    if (primaryScrollAxis) {
                        // we already figured out which way they're scrolling
                        return primaryScrollAxis;
                    }

                    if (gestureEvt && gestureEvt.gesture) {

                        if (!startCoord) {
                            // get the starting point
                            startCoord = ionic.tap.pointerCoord(gestureEvt.gesture.srcEvent);

                        } else {
                            // we already have a starting point, figure out which direction they're going
                            var endCoord = ionic.tap.pointerCoord(gestureEvt.gesture.srcEvent);

                            var xDistance = Math.abs(endCoord.x - startCoord.x);
                            var yDistance = Math.abs(endCoord.y - startCoord.y);

                            var scrollAxis = (xDistance < yDistance ? 'y' : 'x');

                            if (Math.max(xDistance, yDistance) > 30) {
                                // ok, we pretty much know which way they're going
                                // let's lock it in
                                primaryScrollAxis = scrollAxis;
                            }

                            return scrollAxis;
                        }
                    }
                    return 'y';
                }

                var content = {
                    element: element[0],
                    onDrag: function () { },
                    endDrag: function () { },
                    setCanScroll: function (canScroll) {
                        var c = $element[0].querySelector('.scroll');

                        if (!c) {
                            return;
                        }

                        var content = angular.element(c.parentElement);
                        if (!content) {
                            return;
                        }

                        // freeze our scroll container if we have one
                        var scrollScope = content.scope();
                        scrollScope.scrollCtrl && scrollScope.scrollCtrl.freezeScrollShut(!canScroll);
                    },
                    getTranslateX: function () {
                        return $scope.sideMenuContentTranslateX || 0;
                    },
                    setTranslateX: ionic.animationFrameThrottle(function (amount) {
                        var xTransform = content.offsetX + amount;
                        $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(' + xTransform + 'px,0,0)';
                        $timeout(function () {
                            $scope.sideMenuContentTranslateX = amount;
                        });
                    }),
                    setMarginLeft: ionic.animationFrameThrottle(function (amount) {
                        if (amount) {
                            amount = parseInt(amount, 10);
                            $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(' + amount + 'px,0,0)';
                            $element[0].style.width = ($window.innerWidth - amount) + 'px';
                            content.offsetX = amount;
                        } else {
                            $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(0,0,0)';
                            $element[0].style.width = '';
                            content.offsetX = 0;
                        }
                    }),
                    setMarginRight: ionic.animationFrameThrottle(function (amount) {
                        if (amount) {
                            amount = parseInt(amount, 10);
                            $element[0].style.width = ($window.innerWidth - amount) + 'px';
                            content.offsetX = amount;
                        } else {
                            $element[0].style.width = '';
                            content.offsetX = 0;
                        }
                        // reset incase left gets grabby
                        $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(0,0,0)';
                    }),
                    setMarginLeftAndRight: ionic.animationFrameThrottle(function (amountLeft, amountRight) {
                        amountLeft = amountLeft && parseInt(amountLeft, 10) || 0;
                        amountRight = amountRight && parseInt(amountRight, 10) || 0;

                        var amount = amountLeft + amountRight;

                        if (amount > 0) {
                            $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(' + amountLeft + 'px,0,0)';
                            $element[0].style.width = ($window.innerWidth - amount) + 'px';
                            content.offsetX = amountLeft;
                        } else {
                            $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(0,0,0)';
                            $element[0].style.width = '';
                            content.offsetX = 0;
                        }
                        // reset incase left gets grabby
                        //$element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(0,0,0)';
                    }),
                    enableAnimation: function () {
                        $scope.animationEnabled = true;
                        $element[0].classList.add('menu-animated');
                    },
                    disableAnimation: function () {
                        $scope.animationEnabled = false;
                        $element[0].classList.remove('menu-animated');
                    },
                    offsetX: 0
                };

                sideMenuCtrl.setContent(content);

                // add gesture handlers
                var gestureOpts = { stop_browser_behavior: false };
                gestureOpts.prevent_default_directions = ['left', 'right'];
                var contentTapGesture = $ionicGesture.on('tap', onContentTap, $element, gestureOpts);
                var dragRightGesture = $ionicGesture.on('dragright', onDragX, $element, gestureOpts);
                var dragLeftGesture = $ionicGesture.on('dragleft', onDragX, $element, gestureOpts);
                var dragUpGesture = $ionicGesture.on('dragup', onDragY, $element, gestureOpts);
                var dragDownGesture = $ionicGesture.on('dragdown', onDragY, $element, gestureOpts);
                var releaseGesture = $ionicGesture.on('release', onDragRelease, $element, gestureOpts);

                // Cleanup
                $scope.$on('$destroy', function () {
                    if (content) {
                        content.element = null;
                        content = null;
                    }
                    $ionicGesture.off(dragLeftGesture, 'dragleft', onDragX);
                    $ionicGesture.off(dragRightGesture, 'dragright', onDragX);
                    $ionicGesture.off(dragUpGesture, 'dragup', onDragY);
                    $ionicGesture.off(dragDownGesture, 'dragdown', onDragY);
                    $ionicGesture.off(releaseGesture, 'release', onDragRelease);
                    $ionicGesture.off(contentTapGesture, 'tap', onContentTap);
                });
            }
        }
    };
} ]);

    IonicModule

    /**
    * @ngdoc directive
    * @name ionSideMenus
    * @module ionic
    * @delegate ionic.service:$ionicSideMenuDelegate
    * @restrict E
    *
    * @description
    * A container element for side menu(s) and the main content. Allows the left and/or right side menu
    * to be toggled by dragging the main content area side to side.
    *
    * To automatically close an opened menu, you can add the {@link ionic.directive:menuClose} attribute
    * directive. The `menu-close` attribute is usually added to links and buttons within
    * `ion-side-menu-content`, so that when the element is clicked, the opened side menu will
    * automatically close.
    *
    * "Burger Icon" toggles can be added to the header with the {@link ionic.directive:menuToggle}
    * attribute directive. Clicking the toggle will open and close the side menu like the `menu-close`
    * directive. The side menu will automatically hide on child pages, but can be overridden with the
    * enable-menu-with-back-views attribute mentioned below.
    *
    * By default, side menus are hidden underneath their side menu content and can be opened by swiping
    * the content left or right or by toggling a button to show the side menu. Additionally, by adding the
    * {@link ionic.directive:exposeAsideWhen} attribute directive to an
    * {@link ionic.directive:ionSideMenu} element directive, a side menu can be given instructions about
    * "when" the menu should be exposed (always viewable).
    *
    * ![Side Menu](http://ionicframework.com.s3.amazonaws.com/docs/controllers/sidemenu.gif)
    *
    * For more information on side menus, check out:
    *
    * - {@link ionic.directive:ionSideMenuContent}
    * - {@link ionic.directive:ionSideMenu}
    * - {@link ionic.directive:menuToggle}
    * - {@link ionic.directive:menuClose}
    * - {@link ionic.directive:exposeAsideWhen}
    *
    * @usage
    * To use side menus, add an `<ion-side-menus>` parent element. This will encompass all pages that have a
    * side menu, and have at least 2 child elements: 1 `<ion-side-menu-content>` for the center content,
    * and one or more `<ion-side-menu>` directives for each side menu(left/right) that you wish to place.
    *
    * ```html
    * <ion-side-menus>
    *   <!-- Left menu -->
    *   <ion-side-menu side="left">
    *   </ion-side-menu>
    *
    *   <ion-side-menu-content>
    *   <!-- Main content, usually <ion-nav-view> -->
    *   </ion-side-menu-content>
    *
    *   <!-- Right menu -->
    *   <ion-side-menu side="right">
    *   </ion-side-menu>
    *
    * </ion-side-menus>
    * ```
    * ```js
    * function ContentController($scope, $ionicSideMenuDelegate) {
    *   $scope.toggleLeft = function() {
    *     $ionicSideMenuDelegate.toggleLeft();
    *   };
    * }
    * ```
    *
    * @param {bool=} enable-menu-with-back-views Determines whether the side menu is enabled when the
    * back button is showing. When set to `false`, any {@link ionic.directive:menuToggle} will be hidden,
    * and the user cannot swipe to open the menu. When going back to the root page of the side menu (the
    * page without a back button visible), then any menuToggle buttons will show again, and menus will be
    * enabled again.
    * @param {string=} delegate-handle The handle used to identify this side menu
    * with {@link ionic.service:$ionicSideMenuDelegate}.
    *
    */
.directive('ionSideMenus', ['$ionicBody', function ($ionicBody) {
    return {
        restrict: 'ECA',
        controller: '$ionicSideMenus',
        compile: function (element, attr) {
            attr.$set('class', (attr['class'] || '') + ' view');

            return { pre: prelink };
            function prelink($scope, $element, $attrs, ctrl) {

                ctrl.enableMenuWithBackViews($scope.$eval($attrs.enableMenuWithBackViews));

                $scope.$on('$ionicExposeAside', function (evt, isAsideExposed) {
                    if (!$scope.$exposeAside) $scope.$exposeAside = {};
                    $scope.$exposeAside.active = isAsideExposed;
                    $ionicBody.enableClass(isAsideExposed, 'aside-open');
                });

                $scope.$on('$ionicView.beforeEnter', function (ev, d) {
                    if (d.historyId) {
                        $scope.$activeHistoryId = d.historyId;
                    }
                });

                $scope.$on('$destroy', function () {
                    $ionicBody.removeClass('menu-open', 'aside-open');
                });

            }
        }
    };
} ]);


    /**
    * @ngdoc directive
    * @name ionSlideBox
    * @module ionic
    * @codepen AjgEB
    * @deprecated will be removed in the next Ionic release in favor of the new ion-slides component.
    * Don't depend on the internal behavior of this widget.
    * @delegate ionic.service:$ionicSlideBoxDelegate
    * @restrict E
    * @description
    * The Slide Box is a multi-page container where each page can be swiped or dragged between:
    *
    *
    * @usage
    * ```html
    * <ion-slide-box on-slide-changed="slideHasChanged($index)">
    *   <ion-slide>
    *     <div class="box blue"><h1>BLUE</h1></div>
    *   </ion-slide>
    *   <ion-slide>
    *     <div class="box yellow"><h1>YELLOW</h1></div>
    *   </ion-slide>
    *   <ion-slide>
    *     <div class="box pink"><h1>PINK</h1></div>
    *   </ion-slide>
    * </ion-slide-box>
    * ```
    *
    * @param {string=} delegate-handle The handle used to identify this slideBox
    * with {@link ionic.service:$ionicSlideBoxDelegate}.
    * @param {boolean=} does-continue Whether the slide box should loop.
    * @param {boolean=} auto-play Whether the slide box should automatically slide. Default true if does-continue is true.
    * @param {number=} slide-interval How many milliseconds to wait to change slides (if does-continue is true). Defaults to 4000.
    * @param {boolean=} show-pager Whether a pager should be shown for this slide box. Accepts expressions via `show-pager="{{shouldShow()}}"`. Defaults to true.
    * @param {expression=} pager-click Expression to call when a pager is clicked (if show-pager is true). Is passed the 'index' variable.
    * @param {expression=} on-slide-changed Expression called whenever the slide is changed.  Is passed an '$index' variable.
    * @param {expression=} active-slide Model to bind the current slide index to.
    */
    IonicModule
.directive('ionSlideBox', [
  '$animate',
  '$timeout',
  '$compile',
  '$ionicSlideBoxDelegate',
  '$ionicHistory',
  '$ionicScrollDelegate',
function ($animate, $timeout, $compile, $ionicSlideBoxDelegate, $ionicHistory, $ionicScrollDelegate) {
    return {
        restrict: 'E',
        replace: true,
        transclude: true,
        scope: {
            autoPlay: '=',
            doesContinue: '@',
            slideInterval: '@',
            showPager: '@',
            pagerClick: '&',
            disableScroll: '@',
            onSlideChanged: '&',
            activeSlide: '=?',
            bounce: '@'
        },
        controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
            var _this = this;

            var continuous = $scope.$eval($scope.doesContinue) === true;
            var bouncing = ($scope.$eval($scope.bounce) !== false); //Default to true
            var shouldAutoPlay = isDefined($attrs.autoPlay) ? !!$scope.autoPlay : false;
            var slideInterval = shouldAutoPlay ? $scope.$eval($scope.slideInterval) || 4000 : 0;

            var slider = new ionic.views.Slider({
                el: $element[0],
                auto: slideInterval,
                continuous: continuous,
                startSlide: $scope.activeSlide,
                bouncing: bouncing,
                slidesChanged: function () {
                    $scope.currentSlide = slider.currentIndex();

                    // Try to trigger a digest
                    $timeout(function () { });
                },
                callback: function (slideIndex) {
                    $scope.currentSlide = slideIndex;
                    $scope.onSlideChanged({ index: $scope.currentSlide, $index: $scope.currentSlide });
                    $scope.$parent.$broadcast('slideBox.slideChanged', slideIndex);
                    $scope.activeSlide = slideIndex;
                    // Try to trigger a digest
                    $timeout(function () { });
                },
                onDrag: function () {
                    freezeAllScrolls(true);
                },
                onDragEnd: function () {
                    freezeAllScrolls(false);
                }
            });

            function freezeAllScrolls(shouldFreeze) {
                if (shouldFreeze && !_this.isScrollFreeze) {
                    $ionicScrollDelegate.freezeAllScrolls(shouldFreeze);

                } else if (!shouldFreeze && _this.isScrollFreeze) {
                    $ionicScrollDelegate.freezeAllScrolls(false);
                }
                _this.isScrollFreeze = shouldFreeze;
            }

            slider.enableSlide($scope.$eval($attrs.disableScroll) !== true);

            $scope.$watch('activeSlide', function (nv) {
                if (isDefined(nv)) {
                    slider.slide(nv);
                }
            });

            $scope.$on('slideBox.nextSlide', function () {
                slider.next();
            });

            $scope.$on('slideBox.prevSlide', function () {
                slider.prev();
            });

            $scope.$on('slideBox.setSlide', function (e, index) {
                slider.slide(index);
            });

            //Exposed for testing
            this.__slider = slider;

            var deregisterInstance = $ionicSlideBoxDelegate._registerInstance(
        slider, $attrs.delegateHandle, function () {
            return $ionicHistory.isActiveScope($scope);
        }
      );
            $scope.$on('$destroy', function () {
                deregisterInstance();
                slider.kill();
            });

            this.slidesCount = function () {
                return slider.slidesCount();
            };

            this.onPagerClick = function (index) {
                $scope.pagerClick({ index: index });
            };

            $timeout(function () {
                slider.load();
            });
        } ],
        template: '<div class="slider">' +
      '<div class="slider-slides" ng-transclude>' +
      '</div>' +
    '</div>',

        link: function ($scope, $element, $attr) {
            // Disable ngAnimate for slidebox and its children
            $animate.enabled($element, false);

            // if showPager is undefined, show the pager
            if (!isDefined($attr.showPager)) {
                $scope.showPager = true;
                getPager().toggleClass('hide', !true);
            }

            $attr.$observe('showPager', function (show) {
                if (show === undefined) return;
                show = $scope.$eval(show);
                getPager().toggleClass('hide', !show);
            });

            var pager;
            function getPager() {
                if (!pager) {
                    var childScope = $scope.$new();
                    pager = jqLite('<ion-pager></ion-pager>');
                    $element.append(pager);
                    pager = $compile(pager)(childScope);
                }
                return pager;
            }
        }
    };
} ])
.directive('ionSlide', function () {
    return {
        restrict: 'E',
        require: '?^ionSlideBox',
        compile: function (element) {
            element.addClass('slider-slide');
        }
    };
})

.directive('ionPager', function () {
    return {
        restrict: 'E',
        replace: true,
        require: '^ionSlideBox',
        template: '<div class="slider-pager"><span class="slider-pager-page" ng-repeat="slide in numSlides() track by $index" ng-class="{active: $index == currentSlide}" ng-click="pagerClick($index)"><i class="icon ion-record"></i></span></div>',
        link: function ($scope, $element, $attr, slideBox) {
            var selectPage = function (index) {
                var children = $element[0].children;
                var length = children.length;
                for (var i = 0; i < length; i++) {
                    if (i == index) {
                        children[i].classList.add('active');
                    } else {
                        children[i].classList.remove('active');
                    }
                }
            };

            $scope.pagerClick = function (index) {
                slideBox.onPagerClick(index);
            };

            $scope.numSlides = function () {
                return new Array(slideBox.slidesCount());
            };

            $scope.$watch('currentSlide', function (v) {
                selectPage(v);
            });
        }
    };

});


    /**
    * @ngdoc directive
    * @name ionSlides
    * @module ionic
    * @delegate ionic.service:$ionicSlideBoxDelegate
    * @restrict E
    * @description
    * The Slides component is a powerful multi-page container where each page can be swiped or dragged between.
    *
    * Note: this is a new version of the Ionic Slide Box based on the [Swiper](http://www.idangero.us/swiper/#.Vmc1J-ODFBc) widget from
    * [idangerous](http://www.idangero.us/).
    *
    * ![SlideBox](http://ionicframework.com.s3.amazonaws.com/docs/controllers/slideBox.gif)
    *
    * @usage
    * ```html
    * <ion-content scroll="false">
    *   <ion-slides  options="options" slider="data.slider">
    *     <ion-slide-page>
    *       <div class="box blue"><h1>BLUE</h1></div>
    *     </ion-slide-page>
    *     <ion-slide-page>
    *       <div class="box yellow"><h1>YELLOW</h1></div>
    *     </ion-slide-page>
    *     <ion-slide-page>
    *       <div class="box pink"><h1>PINK</h1></div>
    *     </ion-slide-page>
    *   </ion-slides>
    * </ion-content>
    * ```
    *
    * ```js
    * $scope.options = {
    *   loop: false,
    *   effect: 'fade',
    *   speed: 500,
    * }
    *
    * $scope.$on("$ionicSlides.sliderInitialized", function(event, data){
    *   // data.slider is the instance of Swiper
    *   $scope.slider = data.slider;
    * });
    *
    * $scope.$on("$ionicSlides.slideChangeStart", function(event, data){
    *   console.log('Slide change is beginning');
    * });
    *
    * $scope.$on("$ionicSlides.slideChangeEnd", function(event, data){
    *   // note: the indexes are 0-based
    *   $scope.activeIndex = data.slider.activeIndex;
    *   $scope.previousIndex = data.slider.previousIndex;
    * });
    *
    * ```
    *
    * ## Slide Events
    *
    * The slides component dispatches events when the active slide changes
    *
    * <table class="table">
    *   <tr>
    *     <td><code>$ionicSlides.slideChangeStart</code></td>
    *     <td>This event is emitted when a slide change begins</td>
    *   </tr>
    *   <tr>
    *     <td><code>$ionicSlides.slideChangeEnd</code></td>
    *     <td>This event is emitted when a slide change completes</td>
    *   </tr>
    *   <tr>
    *     <td><code>$ionicSlides.sliderInitialized</code></td>
    *     <td>This event is emitted when the slider is initialized. It provides access to an instance of the slider.</td>
    *   </tr>
    * </table>
    *
    *
    * ## Updating Slides Dynamically
    * When applying data to the slider at runtime, typically everything will work as expected.
    *
    * In the event that the slides are looped, use the `updateLoop` method on the slider to ensure the slides update correctly.
    *
    * ```
    * $scope.$on("$ionicSlides.sliderInitialized", function(event, data){
    *   // grab an instance of the slider
    *   $scope.slider = data.slider;
    * });
    *
    * function dataChangeHandler(){
    *   // call this function when data changes, such as an HTTP request, etc
    *   if ( $scope.slider ){
    *     $scope.slider.updateLoop();
    *   }
    * }
    * ```
    *
    */
    IonicModule
.directive('ionSlides', [
  '$animate',
  '$timeout',
  '$compile',
function ($animate, $timeout, $compile) {
    return {
        restrict: 'E',
        transclude: true,
        scope: {
            options: '=',
            slider: '='
        },
        template: '<div class="swiper-container">' +
      '<div class="swiper-wrapper" ng-transclude>' +
      '</div>' +
        '<div ng-hide="!showPager" class="swiper-pagination"></div>' +
      '</div>',
        controller: ['$scope', '$element', function ($scope, $element) {
            var _this = this;

            this.update = function () {
                $timeout(function () {
                    if (!_this.__slider) {
                        return;
                    }

                    _this.__slider.update();
                    if (_this._options.loop) {
                        _this.__slider.createLoop();
                    }

                    var slidesLength = _this.__slider.slides.length;

                    // Don't allow pager to show with > 10 slides
                    if (slidesLength > 10) {
                        $scope.showPager = false;
                    }

                    // When slide index is greater than total then slide to last index
                    if (_this.__slider.activeIndex > slidesLength - 1) {
                        _this.__slider.slideTo(slidesLength - 1);
                    }
                });
            };

            this.rapidUpdate = ionic.debounce(function () {
                _this.update();
            }, 50);

            this.getSlider = function () {
                return _this.__slider;
            };

            var options = $scope.options || {};

            var newOptions = angular.extend({
                pagination: $element.children().children()[1],
                paginationClickable: true,
                lazyLoading: true,
                preloadImages: false
            }, options);

            this._options = newOptions;

            $timeout(function () {
                var slider = new ionic.views.Swiper($element.children()[0], newOptions, $scope, $compile);

                $scope.$emit("$ionicSlides.sliderInitialized", { slider: slider });

                _this.__slider = slider;
                $scope.slider = _this.__slider;

                $scope.$on('$destroy', function () {
                    slider.destroy();
                    _this.__slider = null;
                });
            });

            $timeout(function () {
                // if it's a loop, render the slides again just incase
                _this.rapidUpdate();
            }, 200);

        } ],

        link: function ($scope) {
            $scope.showPager = true;
            // Disable ngAnimate for slidebox and its children
            //$animate.enabled(false, $element);
        }
    };
} ])
.directive('ionSlidePage', [function () {
    return {
        restrict: 'E',
        require: '?^ionSlides',
        transclude: true,
        replace: true,
        template: '<div class="swiper-slide" ng-transclude></div>',
        link: function ($scope, $element, $attr, ionSlidesCtrl) {
            ionSlidesCtrl.rapidUpdate();

            $scope.$on('$destroy', function () {
                ionSlidesCtrl.rapidUpdate();
            });
        }
    };
} ]);

    /**
    * @ngdoc directive
    * @name ionSpinner
    * @module ionic
    * @restrict E
    *
    * @description
    * The `ionSpinner` directive provides a variety of animated spinners.
    * Spinners enables you to give your users feedback that the app is
    * processing/thinking/waiting/chillin' out, or whatever you'd like it to indicate.
    * By default, the {@link ionic.directive:ionRefresher} feature uses this spinner, rather
    * than rotating font icons (previously included in [ionicons](http://ionicons.com/)).
    * While font icons are great for simple or stationary graphics, they're not suited to
    * provide great animations, which is why Ionic uses SVG instead.
    *
    * Ionic offers ten spinners out of the box, and by default, it will use the appropriate spinner
    * for the platform on which it's running. Under the hood, the `ionSpinner` directive dynamically
    * builds the required SVG element, which allows Ionic to provide all ten of the animated SVGs
    * within 3KB.
    *
    * <style>
    * .spinner-table {
    *   max-width: 280px;
    * }
    * .spinner-table tbody > tr > th, .spinner-table tbody > tr > td {
    *   vertical-align: middle;
    *   width: 42px;
    *   height: 42px;
    * }
    * .spinner {
    *   stroke: #444;
    *   fill: #444; }
    *   .spinner svg {
    *     width: 28px;
    *     height: 28px; }
    *   .spinner.spinner-inverse {
    *     stroke: #fff;
    *     fill: #fff; }
    *
    * .spinner-android {
    *   stroke: #4b8bf4; }
    *
    * .spinner-ios, .spinner-ios-small {
    *   stroke: #69717d; }
    *
    * .spinner-spiral .stop1 {
    *   stop-color: #fff;
    *   stop-opacity: 0; }
    * .spinner-spiral.spinner-inverse .stop1 {
    *   stop-color: #000; }
    * .spinner-spiral.spinner-inverse .stop2 {
    *   stop-color: #fff; }
    * </style>
    *
    * <script src="http://code.ionicframework.com/nightly/js/ionic.bundle.min.js"></script>
    * <table class="table spinner-table" ng-app="ionic">
    *  <tr>
    *    <th>
    *      <code>android</code>
    *    </th>
    *    <td>
    *      <ion-spinner icon="android"></ion-spinner>
    *    </td>
    *  </tr>
    *  <tr>
    *    <th>
    *      <code>ios</code>
    *    </th>
    *    <td>
    *      <ion-spinner icon="ios"></ion-spinner>
    *    </td>
    *  </tr>
    *  <tr>
    *    <th>
    *      <code>ios-small</code>
    *    </th>
    *    <td>
    *      <ion-spinner icon="ios-small"></ion-spinner>
    *    </td>
    *  </tr>
    *  <tr>
    *    <th>
    *      <code>bubbles</code>
    *    </th>
    *    <td>
    *      <ion-spinner icon="bubbles"></ion-spinner>
    *    </td>
    *  </tr>
    *  <tr>
    *    <th>
    *      <code>circles</code>
    *    </th>
    *    <td>
    *      <ion-spinner icon="circles"></ion-spinner>
    *    </td>
    *  </tr>
    *  <tr>
    *    <th>
    *      <code>crescent</code>
    *    </th>
    *    <td>
    *      <ion-spinner icon="crescent"></ion-spinner>
    *    </td>
    *  </tr>
    *  <tr>
    *    <th>
    *      <code>dots</code>
    *    </th>
    *    <td>
    *      <ion-spinner icon="dots"></ion-spinner>
    *    </td>
    *  </tr>
    *  <tr>
    *    <th>
    *      <code>lines</code>
    *    </th>
    *    <td>
    *      <ion-spinner icon="lines"></ion-spinner>
    *    </td>
    *  </tr>
    *  <tr>
    *    <th>
    *      <code>ripple</code>
    *    </th>
    *    <td>
    *      <ion-spinner icon="ripple"></ion-spinner>
    *    </td>
    *  </tr>
    *  <tr>
    *    <th>
    *      <code>spiral</code>
    *    </th>
    *    <td>
    *      <ion-spinner icon="spiral"></ion-spinner>
    *    </td>
    *  </tr>
    * </table>
    *
    * Each spinner uses SVG with SMIL animations, however, the Android spinner also uses JavaScript
    * so it also works on Android 4.0-4.3. Additionally, each spinner can be styled with CSS,
    * and scaled to any size.
    *
    *
    * @usage
    * The following code would use the default spinner for the platform it's running from. If it's neither
    * iOS or Android, it'll default to use `ios`.
    *
    * ```html
    * <ion-spinner></ion-spinner>
    * ```
    *
    * By setting the `icon` attribute, you can specify which spinner to use, no matter what
    * the platform is.
    *
    * ```html
    * <ion-spinner icon="spiral"></ion-spinner>
    * ```
    *
    * ## Spinner Colors
    * Like with most of Ionic's other components, spinners can also be styled using
    * Ionic's standard color naming convention. For example:
    *
    * ```html
    * <ion-spinner class="spinner-energized"></ion-spinner>
    * ```
    *
    *
    * ## Styling SVG with CSS
    * One cool thing about SVG is its ability to be styled with CSS! Some of the properties
    * have different names, for example, SVG uses the term `stroke` instead of `border`, and
    * `fill` instead of `background-color`.
    *
    * ```css
    * .spinner svg {
    *   width: 28px;
    *   height: 28px;
    *   stroke: #444;
    *   fill: #444;
    * }
    * ```
    *
    */
    IonicModule
.directive('ionSpinner', function () {
    return {
        restrict: 'E',
        controller: '$ionicSpinner',
        link: function ($scope, $element, $attrs, ctrl) {
            var spinnerName = ctrl.init();
            $element.addClass('spinner spinner-' + spinnerName);

            $element.on('$destroy', function onDestroy() {
                ctrl.stop();
            });
        }
    };
});

    /**
    * @ngdoc directive
    * @name ionTab
    * @module ionic
    * @restrict E
    * @parent ionic.directive:ionTabs
    *
    * @description
    * Contains a tab's content.  The content only exists while the given tab is selected.
    *
    * Each ionTab has its own view history.
    *
    * @usage
    * ```html
    * <ion-tab
    *   title="Tab!"
    *   icon="my-icon"
    *   href="#/tab/tab-link"
    *   on-select="onTabSelected()"
    *   on-deselect="onTabDeselected()">
    * </ion-tab>
    * ```
    * For a complete, working tab bar example, see the {@link ionic.directive:ionTabs} documentation.
    *
    * @param {string} title The title of the tab.
    * @param {string=} href The link that this tab will navigate to when tapped.
    * @param {string=} icon The icon of the tab. If given, this will become the default for icon-on and icon-off.
    * @param {string=} icon-on The icon of the tab while it is selected.
    * @param {string=} icon-off The icon of the tab while it is not selected.
    * @param {expression=} badge The badge to put on this tab (usually a number).
    * @param {expression=} badge-style The style of badge to put on this tab (eg: badge-positive).
    * @param {expression=} on-select Called when this tab is selected.
    * @param {expression=} on-deselect Called when this tab is deselected.
    * @param {expression=} ng-click By default, the tab will be selected on click. If ngClick is set, it will not.  You can explicitly switch tabs using {@link ionic.service:$ionicTabsDelegate#select $ionicTabsDelegate.select()}.
    * @param {expression=} hidden Whether the tab is to be hidden or not.
    * @param {expression=} disabled Whether the tab is to be disabled or not.
    */
    IonicModule
.directive('ionTab', [
  '$compile',
  '$ionicConfig',
  '$ionicBind',
  '$ionicViewSwitcher',
function ($compile, $ionicConfig, $ionicBind, $ionicViewSwitcher) {

    //Returns ' key="value"' if value exists
    function attrStr(k, v) {
        return isDefined(v) ? ' ' + k + '="' + v + '"' : '';
    }
    return {
        restrict: 'E',
        require: ['^ionTabs', 'ionTab'],
        controller: '$ionicTab',
        scope: true,
        compile: function (element, attr) {

            //We create the tabNavTemplate in the compile phase so that the
            //attributes we pass down won't be interpolated yet - we want
            //to pass down the 'raw' versions of the attributes
            var tabNavTemplate = '<ion-tab-nav' +
        attrStr('ng-click', attr.ngClick) +
        attrStr('title', attr.title) +
        attrStr('icon', attr.icon) +
        attrStr('icon-on', attr.iconOn) +
        attrStr('icon-off', attr.iconOff) +
        attrStr('badge', attr.badge) +
        attrStr('badge-style', attr.badgeStyle) +
        attrStr('hidden', attr.hidden) +
        attrStr('disabled', attr.disabled) +
        attrStr('class', attr['class']) +
        '></ion-tab-nav>';

            //Remove the contents of the element so we can compile them later, if tab is selected
            var tabContentEle = document.createElement('div');
            for (var x = 0; x < element[0].children.length; x++) {
                tabContentEle.appendChild(element[0].children[x].cloneNode(true));
            }
            var childElementCount = tabContentEle.childElementCount;
            element.empty();

            var navViewName, isNavView;
            if (childElementCount) {
                if (tabContentEle.children[0].tagName === 'ION-NAV-VIEW') {
                    // get the name if it's a nav-view
                    navViewName = tabContentEle.children[0].getAttribute('name');
                    tabContentEle.children[0].classList.add('view-container');
                    isNavView = true;
                }
                if (childElementCount === 1) {
                    // make the 1 child element the primary tab content container
                    tabContentEle = tabContentEle.children[0];
                }
                if (!isNavView) tabContentEle.classList.add('pane');
                tabContentEle.classList.add('tab-content');
            }

            return function link($scope, $element, $attr, ctrls) {
                var childScope;
                var childElement;
                var tabsCtrl = ctrls[0];
                var tabCtrl = ctrls[1];
                var isTabContentAttached = false;
                $scope.$tabSelected = false;

                $ionicBind($scope, $attr, {
                    onSelect: '&',
                    onDeselect: '&',
                    title: '@',
                    uiSref: '@',
                    href: '@'
                });

                tabsCtrl.add($scope);
                $scope.$on('$destroy', function () {
                    if (!$scope.$tabsDestroy) {
                        // if the containing ionTabs directive is being destroyed
                        // then don't bother going through the controllers remove
                        // method, since remove will reset the active tab as each tab
                        // is being destroyed, causing unnecessary view loads and transitions
                        tabsCtrl.remove($scope);
                    }
                    tabNavElement.isolateScope().$destroy();
                    tabNavElement.remove();
                    tabNavElement = tabContentEle = childElement = null;
                });

                //Remove title attribute so browser-tooltip does not apear
                $element[0].removeAttribute('title');

                if (navViewName) {
                    tabCtrl.navViewName = $scope.navViewName = navViewName;
                }
                $scope.$on('$stateChangeSuccess', selectIfMatchesState);
                selectIfMatchesState();
                function selectIfMatchesState() {
                    if (tabCtrl.tabMatchesState()) {
                        tabsCtrl.select($scope, false);
                    }
                }

                var tabNavElement = jqLite(tabNavTemplate);
                tabNavElement.data('$ionTabsController', tabsCtrl);
                tabNavElement.data('$ionTabController', tabCtrl);
                tabsCtrl.$tabsElement.append($compile(tabNavElement)($scope));


                function tabSelected(isSelected) {
                    if (isSelected && childElementCount) {
                        // this tab is being selected

                        // check if the tab is already in the DOM
                        // only do this if the tab has child elements
                        if (!isTabContentAttached) {
                            // tab should be selected and is NOT in the DOM
                            // create a new scope and append it
                            childScope = $scope.$new();
                            childElement = jqLite(tabContentEle);
                            $ionicViewSwitcher.viewEleIsActive(childElement, true);
                            tabsCtrl.$element.append(childElement);
                            $compile(childElement)(childScope);
                            isTabContentAttached = true;
                        }

                        // remove the hide class so the tabs content shows up
                        $ionicViewSwitcher.viewEleIsActive(childElement, true);

                    } else if (isTabContentAttached && childElement) {
                        // this tab should NOT be selected, and it is already in the DOM

                        if ($ionicConfig.views.maxCache() > 0) {
                            // keep the tabs in the DOM, only css hide it
                            $ionicViewSwitcher.viewEleIsActive(childElement, false);

                        } else {
                            // do not keep tabs in the DOM
                            destroyTab();
                        }

                    }
                }

                function destroyTab() {
                    childScope && childScope.$destroy();
                    isTabContentAttached && childElement && childElement.remove();
                    tabContentEle.innerHTML = '';
                    isTabContentAttached = childScope = childElement = null;
                }

                $scope.$watch('$tabSelected', tabSelected);

                $scope.$on('$ionicView.afterEnter', function () {
                    $ionicViewSwitcher.viewEleIsActive(childElement, $scope.$tabSelected);
                });

                $scope.$on('$ionicView.clearCache', function () {
                    if (!$scope.$tabSelected) {
                        destroyTab();
                    }
                });

            };
        }
    };
} ]);

    IonicModule
.directive('ionTabNav', [function () {
    return {
        restrict: 'E',
        replace: true,
        require: ['^ionTabs', '^ionTab'],
        template:
    '<a ng-class="{\'has-badge\':badge, \'tab-hidden\':isHidden(), \'tab-item-active\': isTabActive()}" ' +
      ' ng-disabled="disabled()" class="tab-item">' +
      '<span class="badge {{badgeStyle}}" ng-if="badge">{{badge}}</span>' +
      '<i class="icon {{getIcon()}}" ng-if="getIcon()"></i>' +
      '<span class="tab-title" ng-bind-html="title"></span>' +
    '</a>',
        scope: {
            title: '@',
            icon: '@',
            iconOn: '@',
            iconOff: '@',
            badge: '=',
            hidden: '@',
            disabled: '&',
            badgeStyle: '@',
            'class': '@'
        },
        link: function ($scope, $element, $attrs, ctrls) {
            var tabsCtrl = ctrls[0],
        tabCtrl = ctrls[1];

            //Remove title attribute so browser-tooltip does not apear
            $element[0].removeAttribute('title');

            $scope.selectTab = function (e) {
                e.preventDefault();
                tabsCtrl.select(tabCtrl.$scope, true);
            };
            if (!$attrs.ngClick) {
                $element.on('click', function (event) {
                    $scope.$apply(function () {
                        $scope.selectTab(event);
                    });
                });
            }

            $scope.isHidden = function () {
                if ($attrs.hidden === 'true' || $attrs.hidden === true) return true;
                return false;
            };

            $scope.getIconOn = function () {
                return $scope.iconOn || $scope.icon;
            };
            $scope.getIconOff = function () {
                return $scope.iconOff || $scope.icon;
            };

            $scope.isTabActive = function () {
                return tabsCtrl.selectedTab() === tabCtrl.$scope;
            };

            $scope.getIcon = function () {
                if (tabsCtrl.selectedTab() === tabCtrl.$scope) {
                    // active
                    return $scope.iconOn || $scope.icon;
                }
                else {
                    // inactive
                    return $scope.iconOff || $scope.icon;
                }
            };
        }
    };
} ]);

    /**
    * @ngdoc directive
    * @name ionTabs
    * @module ionic
    * @delegate ionic.service:$ionicTabsDelegate
    * @restrict E
    * @codepen odqCz
    *
    * @description
    * Powers a multi-tabbed interface with a Tab Bar and a set of "pages" that can be tabbed
    * through.
    *
    * Assign any [tabs class](/docs/components#tabs) to the element to define
    * its look and feel.
    *
    * For iOS, tabs will appear at the bottom of the screen. For Android, tabs will be at the top
    * of the screen, below the nav-bar. This follows each OS's design specification, but can be
    * configured with the {@link ionic.provider:$ionicConfigProvider}.
    *
    * See the {@link ionic.directive:ionTab} directive's documentation for more details on
    * individual tabs.
    *
    * Note: do not place ion-tabs inside of an ion-content element; it has been known to cause a
    * certain CSS bug.
    *
    * @usage
    * ```html
    * <ion-tabs class="tabs-positive tabs-icon-top">
    *
    *   <ion-tab title="Home" icon-on="ion-ios-filing" icon-off="ion-ios-filing-outline">
    *     <!-- Tab 1 content -->
    *   </ion-tab>
    *
    *   <ion-tab title="About" icon-on="ion-ios-clock" icon-off="ion-ios-clock-outline">
    *     <!-- Tab 2 content -->
    *   </ion-tab>
    *
    *   <ion-tab title="Settings" icon-on="ion-ios-gear" icon-off="ion-ios-gear-outline">
    *     <!-- Tab 3 content -->
    *   </ion-tab>
    *
    * </ion-tabs>
    * ```
    *
    * @param {string=} delegate-handle The handle used to identify these tabs
    * with {@link ionic.service:$ionicTabsDelegate}.
    */

    IonicModule
.directive('ionTabs', [
  '$ionicTabsDelegate',
  '$ionicConfig',
function ($ionicTabsDelegate, $ionicConfig) {
    return {
        restrict: 'E',
        scope: true,
        controller: '$ionicTabs',
        compile: function (tElement) {
            //We cannot use regular transclude here because it breaks element.data()
            //inheritance on compile
            var innerElement = jqLite('<div class="tab-nav tabs">');
            innerElement.append(tElement.contents());

            tElement.append(innerElement)
              .addClass('tabs-' + $ionicConfig.tabs.position() + ' tabs-' + $ionicConfig.tabs.style());

            return { pre: prelink, post: postLink };
            function prelink($scope, $element, $attr, tabsCtrl) {
                var deregisterInstance = $ionicTabsDelegate._registerInstance(
          tabsCtrl, $attr.delegateHandle, tabsCtrl.hasActiveScope
        );

                tabsCtrl.$scope = $scope;
                tabsCtrl.$element = $element;
                tabsCtrl.$tabsElement = jqLite($element[0].querySelector('.tabs'));

                $scope.$watch(function () { return $element[0].className; }, function (value) {
                    var isTabsTop = value.indexOf('tabs-top') !== -1;
                    var isHidden = value.indexOf('tabs-item-hide') !== -1;
                    $scope.$hasTabs = !isTabsTop && !isHidden;
                    $scope.$hasTabsTop = isTabsTop && !isHidden;
                    $scope.$emit('$ionicTabs.top', $scope.$hasTabsTop);
                });

                function emitLifecycleEvent(ev, data) {
                    ev.stopPropagation();
                    var previousSelectedTab = tabsCtrl.previousSelectedTab();
                    if (previousSelectedTab) {
                        previousSelectedTab.$broadcast(ev.name.replace('NavView', 'Tabs'), data);
                    }
                }

                $scope.$on('$ionicNavView.beforeLeave', emitLifecycleEvent);
                $scope.$on('$ionicNavView.afterLeave', emitLifecycleEvent);
                $scope.$on('$ionicNavView.leave', emitLifecycleEvent);

                $scope.$on('$destroy', function () {
                    // variable to inform child tabs that they're all being blown away
                    // used so that while destorying an individual tab, each one
                    // doesn't select the next tab as the active one, which causes unnecessary
                    // loading of tab views when each will eventually all go away anyway
                    $scope.$tabsDestroy = true;
                    deregisterInstance();
                    tabsCtrl.$tabsElement = tabsCtrl.$element = tabsCtrl.$scope = innerElement = null;
                    delete $scope.$hasTabs;
                    delete $scope.$hasTabsTop;
                });
            }

            function postLink($scope, $element, $attr, tabsCtrl) {
                if (!tabsCtrl.selectedTab()) {
                    // all the tabs have been added
                    // but one hasn't been selected yet
                    tabsCtrl.select(0);
                }
            }
        }
    };
} ]);

    /**
    * @ngdoc directive
    * @name ionTitle
    * @module ionic
    * @restrict E
    *
    * Used for titles in header and nav bars. New in 1.2
    *
    * Identical to <div class="title"> but with future compatibility for Ionic 2
    *
    * @usage
    *
    * ```html
    * <ion-nav-bar>
    *   <ion-title>Hello</ion-title>
    * <ion-nav-bar>
    * ```
    */
    IonicModule
.directive('ionTitle', [function () {
    return {
        restrict: 'E',
        compile: function (element) {
            element.addClass('title');
        }
    };
} ]);


    IonicModule
.directive('ionToggle', [
  '$timeout',
  '$ionicConfig',
function ($timeout, $ionicConfig) {

    return {
        restrict: 'E',
        replace: true,
        require: '?ngModel',
        transclude: true,
        template:
      '<div class="item item-toggle">' +
        '<div ng-transclude></div>' +
        '<label class="toggle">' +
          '<input type="checkbox">' +
          '<div class="track">' +
            '<div class="handle"></div>' +
          '</div>' +
        '</label>' +
      '</div>',

        compile: function (element, attr) {
            var input = element.find('input');
            forEach({
                'name': attr.name,
                'ng-value': attr.ngValue,
                'ng-model': attr.ngModel,
                'ng-checked': attr.ngChecked,
                'ng-disabled': attr.ngDisabled,
                'ng-true-value': attr.ngTrueValue,
                'ng-false-value': attr.ngFalseValue,
                'ng-change': attr.ngChange,
                'ng-required': attr.ngRequired,
                'required': attr.required
            }, function (value, name) {
                if (isDefined(value)) {
                    input.attr(name, value);
                }
            });

            if (attr.toggleClass) {
                element[0].getElementsByTagName('label')[0].classList.add(attr.toggleClass);
            }

            element.addClass('toggle-' + $ionicConfig.form.toggle());

            return function ($scope, $element) {
                var el = $element[0].getElementsByTagName('label')[0];
                var checkbox = el.children[0];
                var track = el.children[1];
                var handle = track.children[0];

                var ngModelController = jqLite(checkbox).controller('ngModel');

                $scope.toggle = new ionic.views.Toggle({
                    el: el,
                    track: track,
                    checkbox: checkbox,
                    handle: handle,
                    onChange: function () {
                        if (ngModelController) {
                            ngModelController.$setViewValue(checkbox.checked);
                            $scope.$apply();
                        }
                    }
                });

                $scope.$on('$destroy', function () {
                    $scope.toggle.destroy();
                });
            };
        }

    };
} ]);

    /**
    * @ngdoc directive
    * @name ionView
    * @module ionic
    * @restrict E
    * @parent ionNavView
    *
    * @description
    * A container for view content and any navigational and header bar information. When a view
    * enters and exits its parent {@link ionic.directive:ionNavView}, the view also emits view
    * information, such as its title, whether the back button should be displayed or not, whether the
    * corresponding {@link ionic.directive:ionNavBar} should be displayed or not, which transition the view
    * should use to animate, and which direction to animate.
    *
    * *Views are cached to improve performance.* When a view is navigated away from, its element is
    * left in the DOM, and its scope is disconnected from the `$watch` cycle. When navigating to a
    * view that is already cached, its scope is reconnected, and the existing element, which was
    * left in the DOM, becomes active again. This can be disabled, or the maximum number of cached
    * views changed in {@link ionic.provider:$ionicConfigProvider}, in the view's `$state` configuration, or
    * as an attribute on the view itself (see below).
    *
    * @usage
    * Below is an example where our page will load with a {@link ionic.directive:ionNavBar} containing
    * "My Page" as the title.
    *
    * ```html
    * <ion-nav-bar></ion-nav-bar>
    * <ion-nav-view>
    *   <ion-view view-title="My Page">
    *     <ion-content>
    *       Hello!
    *     </ion-content>
    *   </ion-view>
    * </ion-nav-view>
    * ```
    *
    * ## View LifeCycle and Events
    *
    * Views can be cached, which means ***controllers normally only load once***, which may
    * affect your controller logic. To know when a view has entered or left, events
    * have been added that are emitted from the view's scope. These events also
    * contain data about the view, such as the title and whether the back button should
    * show. Also contained is transition data, such as the transition type and
    * direction that will be or was used.
    *
    * Life cycle events are emitted upwards from the transitioning view's scope. In some cases, it is
    * desirable for a child/nested view to be notified of the event.
    * For this use case, `$ionicParentView` life cycle events are broadcast downwards.
    *
    * <table class="table">
    *  <tr>
    *   <td><code>$ionicView.loaded</code></td>
    *   <td>The view has loaded. This event only happens once per
    * view being created and added to the DOM. If a view leaves but is cached,
    * then this event will not fire again on a subsequent viewing. The loaded event
    * is good place to put your setup code for the view; however, it is not the
    * recommended event to listen to when a view becomes active.</td>
    *  </tr>
    *  <tr>
    *   <td><code>$ionicView.enter</code></td>
    *   <td>The view has fully entered and is now the active view.
    * This event will fire, whether it was the first load or a cached view.</td>
    *  </tr>
    *  <tr>
    *   <td><code>$ionicView.leave</code></td>
    *   <td>The view has finished leaving and is no longer the
    * active view. This event will fire, whether it is cached or destroyed.</td>
    *  </tr>
    *  <tr>
    *   <td><code>$ionicView.beforeEnter</code></td>
    *   <td>The view is about to enter and become the active view.</td>
    *  </tr>
    *  <tr>
    *   <td><code>$ionicView.beforeLeave</code></td>
    *   <td>The view is about to leave and no longer be the active view.</td>
    *  </tr>
    *  <tr>
    *   <td><code>$ionicView.afterEnter</code></td>
    *   <td>The view has fully entered and is now the active view.</td>
    *  </tr>
    *  <tr>
    *   <td><code>$ionicView.afterLeave</code></td>
    *   <td>The view has finished leaving and is no longer the active view.</td>
    *  </tr>
    *  <tr>
    *   <td><code>$ionicView.unloaded</code></td>
    *   <td>The view's controller has been destroyed and its element has been
    * removed from the DOM.</td>
    *  </tr>
    *  <tr>
    *   <td><code>$ionicParentView.enter</code></td>
    *   <td>The parent view has fully entered and is now the active view.
    * This event will fire, whether it was the first load or a cached view.</td>
    *  </tr>
    *  <tr>
    *   <td><code>$ionicParentView.leave</code></td>
    *   <td>The parent view has finished leaving and is no longer the
    * active view. This event will fire, whether it is cached or destroyed.</td>
    *  </tr>
    *  <tr>
    *   <td><code>$ionicParentView.beforeEnter</code></td>
    *   <td>The parent view is about to enter and become the active view.</td>
    *  </tr>
    *  <tr>
    *   <td><code>$ionicParentView.beforeLeave</code></td>
    *   <td>The parent view is about to leave and no longer be the active view.</td>
    *  </tr>
    *  <tr>
    *   <td><code>$ionicParentView.afterEnter</code></td>
    *   <td>The parent view has fully entered and is now the active view.</td>
    *  </tr>
    *  <tr>
    *   <td><code>$ionicParentView.afterLeave</code></td>
    *   <td>The parent view has finished leaving and is no longer the active view.</td>
    *  </tr>
    * </table>
    *
    * ## LifeCycle Event Usage
    *
    * Below is an example of how to listen to life cycle events and
    * access state parameter data
    *
    * ```js
    * $scope.$on("$ionicView.beforeEnter", function(event, data){
    *    // handle event
    *    console.log("State Params: ", data.stateParams);
    * });
    *
    * $scope.$on("$ionicView.enter", function(event, data){
    *    // handle event
    *    console.log("State Params: ", data.stateParams);
    * });
    *
    * $scope.$on("$ionicView.afterEnter", function(event, data){
    *    // handle event
    *    console.log("State Params: ", data.stateParams);
    * });
    * ```
    *
    * ## Caching
    *
    * Caching can be disabled and enabled in multiple ways. By default, Ionic will
    * cache a maximum of 10 views. You can optionally choose to disable caching at
    * either an individual view basis, or by global configuration. Please see the
    * _Caching_ section in {@link ionic.directive:ionNavView} for more info.
    *
    * @param {string=} view-title A text-only title to display on the parent {@link ionic.directive:ionNavBar}.
    * For an HTML title, such as an image, see {@link ionic.directive:ionNavTitle} instead.
    * @param {boolean=} cache-view If this view should be allowed to be cached or not.
    * Please see the _Caching_ section in {@link ionic.directive:ionNavView} for
    * more info. Default `true`
    * @param {boolean=} can-swipe-back If this view should be allowed to use the swipe to go back gesture or not.
    * This does not enable the swipe to go back feature if it is not available for the platform it's running
    * from, or there isn't a previous view. Default `true`
    * @param {boolean=} hide-back-button Whether to hide the back button on the parent
    * {@link ionic.directive:ionNavBar} by default.
    * @param {boolean=} hide-nav-bar Whether to hide the parent
    * {@link ionic.directive:ionNavBar} by default.
    */
    IonicModule
.directive('ionView', function () {
    return {
        restrict: 'EA',
        priority: 1000,
        controller: '$ionicView',
        compile: function (tElement) {
            tElement.addClass('pane');
            tElement[0].removeAttribute('title');
            return function link($scope, $element, $attrs, viewCtrl) {
                viewCtrl.init();
            };
        }
    };
});

    //////////
    ///add by charles
    //////////
    IonicModule.factory('myCourseFactory', function () {
        var service = {};
        service.getCourseList = function () {
            var courlist = [];
            $.ajax({
                type: "post",
                url: "subjectsList",
                async: false,
                dataType: "json",
                data: {},
                success: function (result) {
                    courlist = result.rows;
                },
                error: function () {
                    alert("data request errror!");
                    courlist = [];
                    return;
                }
            });
            return courlist;
        };
        service.getCourseDetail = function (id) {
            var coursedetail = [];
            $.ajax({
                type: "post",
                url: "findchaptersection",
                async: false,
                dataType: "json",
                data: { id: id },
                success: function (result) {
                    courlist = result;
                },
                error: function () {
                    alert("data request errror!");
                    courlist = [];
                    return;
                }
            });
            return courlist
        }
        service.getVideoPath = function (id) {
            var videopath = {};
            $.ajax({
                type: "post",
                url: "findResource",
                async: false,
                dataType: "json",
                data: { sectionId: id },
                success: function (result) {
                	if(result)
                	{
                		videopath = result[0];
                	}
                    
                },
                error: function () {
                    alert("data request errror!");
                    videopath = {};
                    return;
                }
            });
            return videopath
        }
        return service;
    });

    IonicModule.controller("myCourseController", function ($scope, myCourseFactory) {
        $scope.courseList = myCourseFactory.getCourseList();
    }
);

    IonicModule.controller("myCourseDetailController", function ($scope, myCourseFactory) {
        var urlStr = location.search;
        var urlPara = [];
        var parObj = new Object();
        if (urlStr.indexOf("?") != -1) {
            urlPara = urlStr.substr(urlStr.indexOf("?") + 1).split("&");
        }
        for (var i = 0; i < urlPara.length; i++) {
            parObj[urlPara[i].split("=")[0].toString()] = urlPara[i].split("=")[1].toString();
        }
        var item = [];
        item = myCourseFactory.getCourseDetail(parObj["id"]);
        $scope.coursedetail = [];
        $scope.coursedes = {};
        if (item.length > 0) {
        	item[0].id=parObj["id"];
            $scope.coursedes = item[0];
        }
        for (var i = 1; i < item.length; i++) {
            var chapterName = item[i].name;
            var charpts = item[i].chapter;
            for (var j = 0; j < charpts.length; j++) {
                $scope.coursedetail.push({ coid: item.id, chid: charpts[j].id, lstatus: 0, ctext: item[i].name
	    		+ "   " + (j + 1) + ") " + charpts[j].title
                });
            }
        }
    });

    IonicModule.controller("getVedioPathController", function ($scope, $sce, myCourseFactory) {
        var urlStr = location.search;
        var urlPara = [];
        var parObj = new Object();
        $scope.describString="";
        $scope.vedioapth = "";
        if (urlStr.indexOf("?") != -1) {
            urlPara = urlStr.substr(urlStr.indexOf("?") + 1).split("&");
        }
        for (var i = 0; i < urlPara.length; i++) {
            parObj[urlPara[i].split("=")[0].toString()] = urlPara[i].split("=")[1].toString();
        }
        $scope.getdetail=function(){
        	 var item = [];
             item = myCourseFactory.getCourseDetail(parObj["id"]);
             if(item.length>0)
            {
            	 $scope.describString=item[0].subjectRemark
            }
        }
        $scope.getdownload=function(){
           	 $scope.describString="正在火速开发完善中，客官久等了";
       }
        $scope.goback=function(){
        	window.history.back();
        }
        $scope.vedioobj = myCourseFactory.getVideoPath(parObj["sectionId"]);
        if($scope.vedioobj&&$scope.vedioobj!={})
        {
	        $scope.imgPath= $sce.trustAsResourceUrl("\\atta\\"+$scope.vedioobj.imgPath);
	        $scope.vedioapth="\\atta\\"+$scope.vedioobj.filePath;
	        $scope.vedioapth = $sce.trustAsResourceUrl($scope.vedioapth);
        }
        
    }
);
})();